表达式求值顺序不同于运算结合性和优先级。规则的核心在于 顺序点( sequence point ) 。在两个顺序点之间,子表达式求值和副作用的顺序是不确定的。假如代码的结果与求值和副作用发生顺序相关,我们称这样的代码有不确定的行为(unspecified behavior)。
先看一个例子:
void f( int i1, int i2, int i3, int i4 ){
cout<< i1 << ' ' << i2 << ' ' << i3 << ' ' << i4 << endl;
}
int main(){
int i = 0;
f( i++, i++, i++, i++ );
}
这个有四个表达式求值,同时每个表达式都有负作用。这八个操作顺序是任意的(当然对于其中i1 = i++;这是由优先级确定顺序的,但对于i = i++;就不确定了,下面再讲),那么结果如何?未定义。
请用 VC7.1 Debug和 Release 分别测试这同一份代码,结果是不同的:
0 0 0 0 [release]
3 2 1 0 [debug]
另外一个例子:
int main()
{
int a[5] = {1, 2, 3, 4, 5};
int *b = a;
printf("%d, %d, %d, %d, %d\n", *b, *(++b), *(++b), *(b++), *(b++));
b = a;
cout << *b << *(b++) << *(++b) << *(b++) << *(b++) << endl;
return 0;
}
对于以上结果,答案不是唯一的,根据编译器而定,在++或=等操作符范围内,子表达式的求值顺序是不一样的。比如上面程序,各个子表达式什么时候赋值顺序是不确定的。
另外一个例子。
i = ++i + 1; // The behavior is unspecified
这个 表达式( expression )包含3个子表达式( subexpression ):
e1 = ++i
e2 = e1 + 1
i = e2
这三个子表达式都没有顺序点( sequence point ),而 ++ i 和 i = e2 都是有副作用( side effect )的表达式(即虽然++i返回是调用i时值+1,但自身修改i时候在顺序点内不确定,同样对于i=e2也是在顺序点内不确定时间修改i)。由于没有顺序点,语言不保证这两个副作用的顺序。
下面是几个术语的定义:
副作用:C++标准指出:访问一个由可变的左值(volatile lvalue)指派的对象(basic.lval),修改一个对象,调用库I/O函数,或者调用函数等所有这些能够改变执行环境的状态的操作都是副作用。
顺序点:A sequence point defines any point in a computer program's execution at which it is guaranteed that all side effects of previous evaluations will have been performed, and no side effects from subsequent evaluations have yet been performed. They are often mentioned in reference to Cand C++, because the result of some expressions can depend on the order of evaluation of their subexpressions. Adding one or more sequence points is one method of ensuring a consistent result, because this restricts the possible orders of evaluation.
With C++11, the most recent iteration of the C++ programming language, usage of the term sequence point has been replaced by specifying that either one evaluation is sequenced before another, or that two evaluations are unsequenced. The execution of unsequenced evaluations can overlap.
In C and C++, sequence points occur in the following places. (In C++, overloaded operators act like functions, and thus operators that have been overloaded introduce sequence points in the same way as function calls.
- Between evaluation of the left and right operands of the && (logical AND), || (logical OR), and comma operators. For example, in the expression*p++ != 0 && *q++ != 0, all side effects of the sub-expression *p++ != 0 are completed before any attempt to access q.
- Between the evaluation of the first operand of the ternary "question-mark" operator and the second or third operand. For example, in the expressiona = (*p++) ? (*p++) : 0 there is a sequence point after the first *p++, meaning it has already been incremented by the time the second instance is executed.
- At the end of a full expression. This category includes expression statements (such as the assignmenta=b;), return statements, the controlling expressions of if,switch, while, or do-while statements, and all three expressions in afor statement.
- Before a function is entered in a function call. The order in which the arguments are evaluated is not specified, but this sequence point means that all of their side effects are complete before the function is entered. In the expressionf(i++) + g(j++) + h(k++), f is called with a parameter of the original value ofi, but i is incremented before entering the body of f. Similarly, j and k are updated before enteringg and h respectively. However, it is not specified in which orderf(), g(), h() are executed, nor in which orderi, j, k are incremented. Variables j andk in the body of f may or may not have been already incremented. Note that a function callf(a,b,c) is not a use of the comma operator and the order of evaluation fora, b, and c is unspecified.
- At a function return, after the return value is copied into the calling context. (This sequence point is only specified in the C++ standard; it is present only implicitly in C.)
- At the end of an initializer; for example, after the evaluation of 5 in the declarationint a = 5;.
- Between each declarator in each declarator sequence; for example, between the two evaluations ofa++ in int x = a++, y = a++.
以下是百度百科对求值顺序的解释:
求值顺序:
表达式求值顺序不同于运算结合性和优先级。
下面是一个经典例子,被 ISO C99/ C++98 /03 三大标准明确提到:他的结果是不确定(unspecified) 的。
i = ++i + 1; // The behavior is unspecified
在介绍概念之前,我们先解释一下它的结果。这个表达式( expression )包含3个子表达式( subexpression ):
e1 = ++i
e2 = e1 + 1
i = e2
这三个子表达式都没有顺序点( sequence point ),而 ++ i 和 i = e3 都是有副作用( side effect )的表达式。由于没有顺序点,语言不保证这两个副作用的顺序。
更加可怕的是,如果i 是一个内建类型,并在下一个顺序点之前被改写超过一次,那么结果是未定义(undefined)的!比如本例中如果有:
int i = 0x1000fffe;
i = ++i + 1; // The result is undefined!!
你也许会认为他的结果是加1 或者加2,其实更糟糕 —— 结果可能是 0x1001ffff 。他的高字节接受了一个副作用的内容,而低字节则接受了另一个副作用的内容! 如果i 是指针,那么将很容易造成程序崩溃。
为什么要这么做呢?因为对于编译器提供商来说,未确定的顺序对优化有相当重要的作用。比如,一个常见的优化策略是“减少寄存器占用和临时对象”。编译器可以重新组织表达式的求值,以便尽量不使用额外的寄存器以及临时变量。 更加严格的说,即使是编译器提供商也无法完全彻底序列化指令(比如无法严格规定读和写的顺序),因为CPU本身有权利修改指令顺序,以便达到更高的速度。
下面的术语以 ISO C99 和 C++03为准。译名为参考并附带原术语对照,如有解释不当或者错误望指正。
--------------------------------------------------------------------------------
表达式有两种功能。每个表达式都产生一个值( value ),同时可能包含副作用( side effect ),比如可能修改某些值。
规则的核心在于 顺序点( sequence point ) [ C99 6.5 Expressions 条款2 ] [ C++03 5 Expressions 概述 条款4 ]。 这是一个结算点,语言要求这一侧的求值和副作用(除了临时对象的销毁以外)全部完成,才能进入下面的部分。 C/C++中大部分表达式都没有顺序点,只有下面五种表达式有:
1 函数。函数调用之前有一个求值顺序点。
2 && || 和 ?: 这三个包含逻辑的表达式。其左侧逻辑完成后有一个求值顺序点。
3 逗号表达式。逗号左侧有一个求值顺序点。
注意,他们都只有一个求值顺序点,2和3的右侧运算结束后并没有求值顺序点。
在两个顺序点之间,子表达式求值和副作用的顺序是不确定的。假如代码的结果与求值和副作用发生顺序相关,我们称这样的代码有不确定的行为(unspecified behavior)。 而且,假如期间对一个内建类型执行一次以上的写操作,则是未定义行为(undefined behavior)——我们知道,未定义行为带来最好的后果是让你的程序立即崩掉。
n = n++; // 两个副作用,对于内建对象产生是未定义行为
几乎所有表达式,求值顺序都不确定。比如,下面的加法, f1 f2 f3的调用顺序是任意的:
n = f1() + f2() + f3(); // f1 f2 f3 调用顺序任意
而函数也只在实际调用前有一个求值顺序点。所以,常见于早期 C 语言教材的这类题目,是错题:
printf("%d",--a+b,--b+a); // --a + b 和 --b + a 这两个子表达式,求值顺序不确定
天啊,甚至可能出现未定义行为?那么坚决不写与实现相关的代码是最好的对策。即使是不确定行为(比如函数调用时) 只要没有顺序点编译器怎么做方便就怎么做。 有些人认为函数调用参数求值与入栈顺序相关,这是一种误导。这个东西要解释,无异于事后诸葛亮:
void f( int i1, int i2, int i3, int i4 ){
cout<< i1 << ' ' << i2 << ' ' << i3 << ' ' << i4 << endl;
}
int main(){
int i = 0;
f( i++, i++, i++, i++ );
}
这个有四个表达式求值,同时每个表达式都有负作用。这八个操作顺序是任意的,那么结果如何?未定义。
请用 VC7.1 Debug和 Release 分别测试这同一份代码,结果是不同的:
0 0 0 0 [release]
3 2 1 0 [debug]
事实上,鉴于前面的讨论,如果换一些其他初始值,这里甚至会出现错位而得到千奇百怪的诡异结果。
其他例子
再看看C/C++标准中的其他经典例子:
[C99] 6.5.2.2 Function call
条款12 EXAMPLE 在下面的函数调用中:
(*pf[f1()]) ( f2(), f3() + f4() )
函数 f1 f2 f3 和f4 可能以任何顺序被调用。 但是,所有副作用都必须在那个 pf[ f1() ] 返回的函数指针产生的调用前完成。
[C++03] 5 Expressions 概论4
i = v[i++]; // the behavior is unspecified
i = 7, i++, i++; // i becomes 9 ( 译注: 赋值表达式比逗号表达式优先级高 )
i = ++i + 1; // the behavior is unspecified
i = i + 1; // the value of i is incremented
--------------------------------------------------------------------------------
More Effective C++ 告诫我们, 千万不要重载 &&, || 和, 操作符[ MEC ,条款7 ]。为什么?
以逗号操作符为例,每个逗号左侧有一个求值顺序点。假如ar是一个普通的对象,下面的做法是无歧义的:
ar[ i ], ++i ;
但是,如果ar[ i ] 返回一个 class A 对象或引用,而它重载了 operator, 那么结果不妙了。那么,上面的语句实际上是一个函数调用:
ar[ i ].operator, ( ++i );
C/C++ 中,函数只在调用前有一个求值顺序点。所以 ar 和 ++i 的求值、以及 ++i 副作用的顺序是任意的。这会引起混乱。
更可怕的是,重载 && 和 || 。 大家已经习惯了其速死算法: 如果左侧求值已经决定了最终结果,则右侧不会被求值。而且大家很依赖这个行为,比如是C风格字符串拷贝常常这样写:
while( p && *p )
*pd++ = *p++;
假如p 为 0, 那么 *p 的行为是未定义的,可能令程序崩溃。 而 && 的求值顺序避免了这一点。 但是,如果我们重载 && 就等于下面的做法:
exp1 .operator && ( exp2 )
现在不仅仅是求值混乱了。无论exp1是什么结果,exp2 必然会被求值。
--------------------------------------------------------------------------------
模糊定义
最后这里有篇更深入的帖子:C++中的求值|副作用|序列点所导致的模糊语义,推荐看看。
作者在google讨论组上发了篇帖子,看了简直会让你对求值顺序和副作用发疯。比如,下面这个表达式结果居然是不确定的?
cout << "x = " << itoa(42,buf,10) << ", y = " << itoa(43,buf,10);
大家要小心别中招啊... 另外有一个更加难以察觉的例子:
void f ( auto_ptr );
void f_2( auto_ptr, int );
f( auto_ptr( new int ) ); // exception save
f( auto_ptr( new int ), g() ); // !! not exception save
第一个例子是异常安全的。new int 返回一个指针 —— 如果他失败那么内存根本不会分配。 auto_ptr<> 如果失败(如果的话),那么它有能力删除这个指针。
第二个例子则不然,如果g()抛出异常,那么他有可能导致 memleak —— 你看,若求值碰巧以这个顺序进行:
1 int* temp = new int
2 g()
3 auto_ptr( temp )
那么当 g() 抛出异常的时候,temp 是一个裸指针,不会有任何机制帮助它释放其内存!