C/C++中的sequence point和side effect
来源是在网上看到的一段代码,用于交换两个变量值:
01 #include <stdio.h>
02
03 /*
04 Why the first swap operation works as expected but
05 it does not happen the same with the second one ?
06 I guess that it can be due operations within temp values,
07 but IMHO swap operation should work in both cases.
08
09 Thanks !
10 */
11
12 void swap(int *a, int *b) {
13 *a ^= *b ^= *a ^= *b;
14 }
15
16 int main() {
17 int a = 5;
18 int b = 8;
19 printf(“%d, %d\n“, a, b);
20 a ^= b ^= a ^= b;
21 printf(“%d, %d\n“, a, b);
22 swap(&a, &b);
23 printf(“%d, %d\n“, a, b);
24 }
经GCC编译后,代码运行的结果是:
5,8
8,5
0,8
理由是:编译器在翻译第十三行的时候,没能给出正确的解释(可能增加了临时变量之类的,具体的讨论见这里)。
为什么对于第13行和第20行编译器会给出不同的解释?这个就和顺序点(sequence point)和副作用(side effect)有关。我在网上找到了一些关于这两个概念的解释,觉得下面两个稍微容易懂些:
副作用(side effect):
表达式有两种功能:每个表达式都产生一个值( value ),同时可能包含副作用( side effect )。副作用是指改变了某些变量的值。
如:
1: 20 //这个表达式的值是20;它没有副作用,因为它没有改变任何变量的值。
2: x=5 // 这个表达式的值是5;它有一个副作用,因为它改变了变量x的值。
3: x=y++ // 这个表达示有两个副作用,因为改变了两个变量的值。
4: x=x++ // 这个表达式也有两个副作用,因为变量x的值发生了两次改变。
顺序点(sequence point):
顺序点的意思是在一系列步骤中的一个“结算”的点,语言要求这一时刻的求值和副作用全部完成,才能进入下面的部分。在C/C++中只有以下几种存在顺序点:
1) 分号;
2) 未重载的逗号运算符的左操作数赋值之后(即’,'处)
3) 未重载的’||’运算符的左操作数赋值之后(即’||’处);
4) 未重载的’&&’运算符的左操作数赋值之后(即”&&”处);
5) 三元运算符’? : ‘的左操作数赋值之后(即’?'处);
6) 在函数所有参数赋值之后但在函数第一条语句执行之前;
7) 在函数返回值已拷贝给调用者之后但在该函数之外的代码执行之前;
8) 每个基类和成员初始化之后;
9) 在每一个完整的变量声明处有一个顺序点,例如int i, j;中逗号和分号处分别有一个顺序点;
10) for循环控制条件中的两个分号处各有一个顺序点。
C++标准中规定:An expression can modify an object’s value only once between consecutive “sequence points.” (在两个顺序点之间,一个值只能被赋值一次)如果违背了这个规定,那么这两个顺序点之间产生的任何副作用的发生顺序都是未定义的(不被标准限制,依赖于编译器的实现)。
维基百科上有两个例子:
f() + g() 这里只有一个顺序点“;”,函数 f 和函数 g 的执行顺序是undefined的,在不同厂商的编译器中,f 和g 都有可能先执行。
i = i++ 这个例子在一个顺序点之前,对 i 进行了两次修改,违背了上面的规定。因此最后 i 的值也是不确定的。
另外,我还在网上看到了一个说法:标准还规定,如果表达式求值过程会更改某对象的值,那么要求更改前的值被读取的唯一目的,只能是用来确定要存入的新值。(通俗点,也就是如果某个对象会被修改,同时这个对象会被读取用作其他用途,那么就违背了规定。后果就是,读取和修改的发生顺序会被不同的编译器给出不同的解释) 对于这种说法,有两个例子:
x[i] = i++ ,在这个顺序点之前,i 虽然只被修改了一次,但是 i 被读取不是为了确定 i 的新值,而是为了确定数组中被修改元素的下标,这也违背了规定。
function(x, x++) 这里只有一个顺序点,x 只被修改了一次,但是 x 同时被读取作为函数参数,违背了规定,那么 x 就有可能先被加1,再被读取,也有可能先被读取,再被加1。
总之,尽量保证不违反这两个规定,在两个相邻顺序点之间同一个变量不可以被修改两次以上或者同时有读取和修改,否则,就会产生未定义的行为。非要仔细研究这个规则意义不大,写代码的时候写得清晰一点,不要故意炫耀技巧就可以了。
近期评论