今天复习考研的时候在书上看到这样一段话:
printf输出时表达式值的计算顺序是从右到左,例如:
int a = 1;
printf("%d %d %d\n", a, a + 1, a = 3);
输出的结果是3,4,3,而不是1,2,3
看起来似乎没什么问题,很简单,然后我就自己随便写了几条代码测试了一下。
int main()
{
int a = 1;
printf("%d %d %d\n", a=11, a++, a++);
a = 1;
printf("%d %d %d\n", a, a + 1, a = 3);
a = 1;
printf("%d %d %d\n", a++, a++, a = 3);
a = 1;
printf("%d %d %d\n", a = 11, a = 22, a = 33);
}
但是测试结果让我整个人都惊呆了
按照书上的理论来说,从右往左算,那么我们一条一条看:
第一条:
- a++,先输出a=1,然后a=a+1=2。
- a++,先输出a=2,然后a=a+1=3。
- a=11,a赋值11后输出a=11。
- 那么结果就是11 2 1,和测试一致。
第二条:
- a=3,a赋值3后输出a=3。
- a+1,输出a+1=4。
- a=11,输出a=3。
- 这一条就是书上举的例子,结果是3 4 3和测试也一致。
第三条:
- a=3,a赋值3后输出a=3。
- a++,先输出a=3,然后a=a+1=4
- a++,先输出a=4,然后a=a+1=5
- 那么结果就应该是4 3 3,可测试结果却是4 3 5,不一致。
第四条:
- a=33,a赋值33后输出a=33。
- a=22,a赋值22后输出a=22。
- a=11,a赋值11后输出a=11。
- 那么结果就是33 22 11,可测试结果却是11 11 11,不一致。
这是怎么回事?我是按照书上说的从右往左算的呀,为什么答案不对?难道书错了?我打开百度试图找到答案,发现很多博客都在说c语言函数的参数调用是从右往左依次入栈处理的,这和书上说的一致,那为什么第三条和第四条结果不正确呢。我们都知道,在参数入栈的时候,如果参数是一个函数则会先计算这个函数,得到返回值,这个返回值才是真正要入栈的参数。但是我们这里的参数并不是一个函数,而是一个表达式,那么现在问题就来了,比如说a=3,究竟是先把a赋值为3然后再入栈还是先把a入栈再赋值为3?这个问题的答案很简单,肯定是先把a赋值为3,没人会犹豫。那a++呢,++a呢,a+=3呢,a*=2呢,都是先计算后入栈吗?我经过大量的测试得出的答案是:除了a++以外,所有式子都是先计算后入栈。这个答案听起来是不是非常合理?毕竟c语言里只有后置自增运算符是先进行运算再改变自身的值。
可是这个结论我一开始就是这么用的啊,第三条的a++我确实是先入栈后计算的呀,答案仍然不对啊,这个时候就有一个新的问题:是计算一个式子入栈一个式子,还是全部计算完再入栈?后面的式子改变了变量会不会影响前面的式子入栈的值?这个问题困扰了我好久,许多博客都没有分析这个问题,我查阅了好多资料,但无奈实力有限,有些关于参数调用的源码看不懂,所以没法从根本上解决这个问题。但是我根据那些我看得懂的资料和我自己的大量测试,得到了下面这个结论:
函数调用参数的时候,从右往左依次执行所有会改变变量数值的式子,遇到后置自增的式子则直接把此时的变量代入,得到该自增式的值。全部执行完成后再从左往右依次把改变后的变量值代入各个除后置自增式以外的所有式子,此时这些式子的值是最终要代入函数的参数。
虽然这个结论没有理论依据,但是我真的进行了大量测试,没有一个不符合这个结论的,至少正确性上有把握,如果哪位大佬发现了问题请在评论区指教一下。
让我们用这个结论再看一遍第三条和第四条。
第三条:
- a=3,a赋值3。
- a++,是后置自增式,此时a=3,直接代入该式子,该式子值就为3,然后执行a++,a=4。
- a++,是后置自增式,此时a=4,直接代入该式子,该式子值就为4,然后执行a++,a=5。
- 从右往左算完了,再从左往右,两个后置自增式已经处理过,最后就是把a=5代入"a=3"这个式子,此时"=3"的部分我们已经处理过了,所以可以把这个式子就看作a,那么该式子的值就是5。
- 那么结果就应该是4 3 5,测试结果也是4 3 5,一致。
第四条:
- a=33,a赋值33。
- a=22,a赋值22。
- a=11,a赋值11。
- 从左往右,因为这三个式子都不是后置自增式,所以把a=11依次代入这三个式子,同理忽视"=33","=22","=11",那么这三个式子就相当于三个a,那么这三个式子的值都是11。
- 那么结果就是11 11 11,测试结果也是11 11 11,一致。
我再举几个比较复杂的例子,把这几个例子看懂你就能彻底明白我的结论。
int main()
{
int a = 1, b = 3;
printf("%d %d %d\n", a = 3, 3 * (a - 4), 2 * (a++));
a = 1;
printf("%d %d %d\n", a + b--, a = b * (a += 4), b + 2 * (a = 3));
a = 1;
printf("%d %d %d %d\n", ++a, a++, a++, a = 5);
a = 1;
printf("%d %d %d\n", a, (a++)*(a+=2), ++a);
}
第一条:
- 2*(a++),其中a++是后置自增式,所以直接把此时的a=1代入该式子,该式子的值就是2,然后执行a++,a=2。
- 3*(a-4),没有对任何变量作出修改,跳过。
- a=3,a赋值3。
- 从左往右,第一个式子"=3"处理过了,把a=3代入a得该式子的值是3。
- 第二个式子将a=3代入得-3。那么该式子得值就是-3。
- 第三个式子含有后置自增式,已经处理过了,由第一步可知该式子的值是2。
- 所以输出的结果就是3 -3 2。
第二条:
- b + 2 * (a = 3),只有”a=3“部分改变了变量值,所以执行该部分,a赋值3。
- a = b * (a += 4),先执行a+=4,此时a=7,b=3,再执行a = b * (a += 4),得a=21。
- a + b--,b--是后置自增式,直接把此时得a=7和b=3代入,得该式子得值是24,再执行b--,得b=2。
- 从左往右,第一个式子"a + b--"处理过了",由第三步可知该式子的值是24。
- 第二个式子"= b * (a += 4)"都已经处理过了,所以只剩下一个a,将a代入该式子,得该式子得值是21。
- 第三个式子"=3"部分已经处理过,所以相当于是b+2*a,将a和b代入得44。
- 所以输出的结果就是24 21 44。
第三条:
- a = 5,a赋值5。
- a++,后置自增式,直接将a=5代入,得该式子得值为5,再执行a++,得a=6。
- a++,后置自增式,直接将a=6代入,得该式子得值为6,再执行a++,得a=7。
- ++a,前置自增式,执行++a,得a=8。
- 从左往右将a=8往回代,第一个式子就是8。
- 第二个式子第三个式子都是后置自增式,已经处理过了。
- 第四个式子,把a=8代入,得该式子得值是8。
- 所以输出的结果就是8 6 5 8。
第四条:
- ++a,前置自增式,执行++a,得a=2。
- (a++)*(a+=2),先执行a+=2,得a=4,然后a++是后置自增式,此时直接把a=4代入式子中,得到该式子得值是4*4=16,再执行a++,得a=5。
- a,跳过。
- 从左往右把a=5往回代,第一个式子a的值就是5。
- 第二个式子已经处理过,由第2步可知,式子的值是16。
- 第三个式子相当于就是a,第三个式子的值是5。
- 所以输出的结果就是5 16 5。
所有测试符合结论。
其实在不同的编译器上函数的参数调用顺序可能不一致,所以该结论并不是一直适用,我在vs2019和codeblock上测试是没问题的。