为什么突然又想起来了写这么一篇文章?是因为在看THQ的C程序编程的课本时,在看for循环看到一个for(i=1;i<=100;i++,i++)等于for(i=1;i<=100;i=i+2),当时不知为何无法理解。再来看while(i++<100) printf("i=%d",i),i是如何的?先用i值和100比较还是自增完毕后和100比较?
首先副作用是什么?
C primer plus中对副作用(side effect)的定义为:对数据对象或文件的修改。例如i=5;计算这个表达式的副作用就是把i的值变为5;同样我们要讨论的++和--也有副作用。
第二什么是顺序点。
C primer plus中对顺序点的定义为:在该点,所有的副作用都在进入下一步前被计算。例如在C语言中分号就是一个顺序点,它意味着像自增自减赋值等运算符所做的全部改变必须在程序进入下一个语句前发生。
第三完整表达式的概念:如果一个表达式不是一个更大表达式的的子表达式,那么这个表达式就是一个完全表达式。
那么哪些是顺序点呢?(结合维基百科和网上网友的回答)
1)分号;
2)未重载的逗号运算符的左操作数赋值之后(即','处)
3)未重载的'||'运算符的左操作数赋值之后(即'||'处);
4)未重载的'&&'运算符的左操作数赋值之后(即"&&"处);
5)三元运算符'? : '的左操作数赋值之后(即'?'处);
例如,例1:表达式*p++ !=
0 && *q++ != 0
,子表达式*p++ !=
0
的副作用都会在试图访问q
之前完成。 例2:表达式a
= (*p++) ? (*p++) : 0
在第一个*p++
之后存在顺序点,因而在第二个*p++
求值之前已经做完一次自增。(对于这个我想说一下,第一个*p++和第二个*p++都是使用的表达式的值也就是*p的值,这个可以通过对程序的汇编看出来,只不过第一个的值是1,第二个的值是2)
6)在函数所有参数赋值之后但在函数第一条语句执行之前;函数实参的求值顺序未指定,但顺序点意味着这些实参求值的副作用在进入函数时都已经完成。就是这个引起了下列的问题。
7)在函数返回值已拷贝给调用者之后但在该函数之外的代码执行之前;(根据维基所说,现在只有C++指出了这个顺序点)
8)每个基类和成员初始化之后;
9)在每一个完整的变量声明处有一个顺序点 例如,int
x = a++, y = a++
的两次a++
求值之间
10)完整表达式结束处。包括表达式语句(如赋值a=b;
),返回语句,if
、switch
、while
、do
-while
语句的控制表达式,for
语句的3个表达式。while(i++<100) printf("i=%d",i)这个语句,C保证副作用(增加i的值)在程序进入printf()前发生。同时使用后缀形式i在和100比较后才增加。根据《C语言程序设计-现代方法》所讲,for语句的第一个和第三个表达式都是以语句的方式执行的,所以刚好可以利用它们的副作用;其结果就是这两个表达式常常作为赋值表达式或者自增/自减表达式。
C_Primer_Plus(第五版P218)中说当一个函数被调用时,将创建被声明为形式参数的变量,然后用计算后得到的实际参数的值初始化该变量。
C语言程序设计—现代方法(P43)函数调用执行之前实际参数必须全部计算出来。如果实际参数恰巧是含有++或者--运算符的表达式,那么必须在调用发生前进行自增货自减操作
在对顺序点的学习中知道函数调用时的函数入口点是顺序点。既然是顺序点的话那么在进入函数之前,函数的实参求值的副作用在进入函数时都已经完成。
那么问题来了:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a,b;
a=1;
b=test(a++);
printf("a=%d,b=%d",a,b);
system("pause");
return 0;
}
test(x)
{
int z;
z=x;
return z;
}
按照顺序点以及两本书所说的,a++在调用之前已经完成自增,即传给形参的值应该是2.但是实际上结果是 2,1.请问这是为什么?
解答:假设有
虽然你看到的调用代码就一行
其实是有好几条汇编指令的
详细的可见我以前写的关于函数调用的帖子(http://tieba.baidu.com/p/3422836606?pid=60597208037&cid=0#60597208037)
简单的讲就是说最终我们调用函数其实就是一条简单的call指令,之前会做参数的传递等准备工作,有的用压栈方式,有的用寄存器传参等等,根据书中的说法就是,实参的操作需在call指令执行之前完成,但是这个跟传参是无关的
可以看下在vc和cfree下两者的反编译结果
是不是都如书中说的add指令和incl指令都在call之前完成了呢。
根据反编译就可以看出其顺序是
压栈传参
自增
调用
在调用函数时所有准备工作做完到call之前是一个顺序点,在此之前自增或自减的操作会完成。比如说a是一个全局变量,当调用foo(a++)时,会产生好几条汇编指令,其中最后一条是call,那么在这个顺序点前所有的副作用必须完成,也就是说a++已经完成了。那么在进入foo函数后a的值已经自增1了,而不是在函数调用结束之后再自增的。