操作符(运算符)的优先级和结合性并不决定表达式的求值顺序,只是用于进行语法分析,决定语法树的生成。
3 + 4 * 5,可以解析成(3 + 4) * 5 或者 3 + (4 * 5),因为乘法优先级高于加法,所以会选择第二种解析方法。
3 + 4 + 5,可以解析成(3 + 4) + 5 或者 3 + (4 + 5),因为加法是自左向右结合,所以会选择第一种解析方法。
解析成3 + (4 * 5)后,对这个表达式求值,“+”操作符有两个操作数,左操作数为 3,右操作数为 4 * 5,那么是先对左操作数 3 求值呢,还是先对右操作数 4 * 5 求值呢?答案是未指定的,由编译器决定。
标准并没有指定“+”操作符的求值顺序,标准只指定了“,”、“&&”、“||”、“?:”这四种操作符的求值顺序是自左向右,其余的操作符的求值顺序都是未指定的。
因此 3 + (4 * 5),并非先算4 * 5,然后算 3 ,而是由编译器自己决定,求值顺序是未指定的。同理:
3 + 4 * 5,可以解析成(3 + 4) * 5 或者 3 + (4 * 5),因为乘法优先级高于加法,所以会选择第二种解析方法。
3 + 4 + 5,可以解析成(3 + 4) + 5 或者 3 + (4 + 5),因为加法是自左向右结合,所以会选择第一种解析方法。
解析成3 + (4 * 5)后,对这个表达式求值,“+”操作符有两个操作数,左操作数为 3,右操作数为 4 * 5,那么是先对左操作数 3 求值呢,还是先对右操作数 4 * 5 求值呢?答案是未指定的,由编译器决定。
标准并没有指定“+”操作符的求值顺序,标准只指定了“,”、“&&”、“||”、“?:”这四种操作符的求值顺序是自左向右,其余的操作符的求值顺序都是未指定的。
因此 3 + (4 * 5),并非先算4 * 5,然后算 3 ,而是由编译器自己决定,求值顺序是未指定的。同理:
-
int i = f() + g(); // 并非一定先执行f(),后执行g(),而是由编译器决定
所以 ++i + ++i + ++i; 会被解析成 ((++i) + (++i)) + (++i);
这个表达式是一个典型的未定义行为,因为 ++i 有两个作用,第一:对i加1;第二:返回一个值(或者应该说返回一个引用)。这里的对i加1属于 副作用(side effect),而副作用什么时候生效呢? 标准只规定了在跨越一个序列点的时候,序列点之前的副作用必须全部完成,此外就没有硬性要求了。
前置++
{
this -> m_value += 1 ;
return * this ;
}
后置++
this -> m_value += 1 ;
return * this ;
}
后置++
{
Int old = * this ;
++ ( * this );
return old;
}
序列点是一个时间点(在整个表达式全部计算完毕之后或在||、&&、? : 或逗号运算符处, 或在函数调用之前), 此刻尘埃落定, 所有的副作用都已确保结束。
Int old = * this ;
++ ( * this );
return old;
}
序列点是一个时间点(在整个表达式全部计算完毕之后或在||、&&、? : 或逗号运算符处, 或在函数调用之前), 此刻尘埃落定, 所有的副作用都已确保结束。
C中为什么要搞这么一个复杂的序列点机制,让许多看起来很简单的代码变成未定义呢?为什么不详细的定义求值顺序呢?因为C是极端追求效率的语言,它诞生的年代计算机硬件都慢的可怜且贵的离谱,没有规定求值顺序就是要给编译器更大的余地去做优化。
而((++i) + (++i)) + (++i); 这个表达式的序列点就是语句的结束之处,也就是";"所在之处,在此之前副作用何时生效(也就是何时给i加1)完全由编译器决定,编译器可以任意选择,比如:
a, 先计算左边的子表达式((++i) + (++i)):
先对左边的i加1(此时i == 6),再对右边的i加1(i == 7),然后再对左边的表达式求值,得到7,对右边的表达式求值,得到7,对子表达式求值,得到 7 + 7 = 14;
b,再计算右边的子表达式 (++i)
因为此时i == 7,所以++i == 8,子表达式值为8.
c, 计算整个表达式的值:14 + 8 = 22;
编译器还可以选择先对所有的3个i都加1(完成副作用),然后再分别求值,得到8 + 8 + 8 = 24.
当然也可以挨个加1、求值,得到6 + 7 + 8 = 21.
只要满足在((++i) + (++i)) + (++i);结束之后,i == 8(副作用完成)就行了,具体过程标准并没有指定,由编译器自己决定。
至于说这种行为属于未定义,是因为c标准中有这么一条:
在上一个和下一个序列点之间, 一个对象所保存的值至多只能被表达式的计算修改一次。而且前一个值只能用于决定将要保存的值。
而这里的i显然在一条语句中被修改了多次。
而((++i) + (++i)) + (++i); 这个表达式的序列点就是语句的结束之处,也就是";"所在之处,在此之前副作用何时生效(也就是何时给i加1)完全由编译器决定,编译器可以任意选择,比如:
a, 先计算左边的子表达式((++i) + (++i)):
先对左边的i加1(此时i == 6),再对右边的i加1(i == 7),然后再对左边的表达式求值,得到7,对右边的表达式求值,得到7,对子表达式求值,得到 7 + 7 = 14;
b,再计算右边的子表达式 (++i)
因为此时i == 7,所以++i == 8,子表达式值为8.
c, 计算整个表达式的值:14 + 8 = 22;
编译器还可以选择先对所有的3个i都加1(完成副作用),然后再分别求值,得到8 + 8 + 8 = 24.
当然也可以挨个加1、求值,得到6 + 7 + 8 = 21.
只要满足在((++i) + (++i)) + (++i);结束之后,i == 8(副作用完成)就行了,具体过程标准并没有指定,由编译器自己决定。
至于说这种行为属于未定义,是因为c标准中有这么一条:
在上一个和下一个序列点之间, 一个对象所保存的值至多只能被表达式的计算修改一次。而且前一个值只能用于决定将要保存的值。
而这里的i显然在一条语句中被修改了多次。
对于 x=5,y=(++x)+(++x)+(++x),y=?
这是和编译器有关的具体如下,仅供参考:(只列举bc和vc的情况,其他的自己找规律)1、对于赋值计算,如: i = 5; k = (++i) + (i++) + (++i) + (++i);
(1) BC下:
总体原则: 先计算所有的前置++或--,再计算表达式的值,最后计算后置++或--。
a. 先计算所有i的前置++或--,得出i的值为8;
b. 再计算k = 8 + 8 + 8 + 8 = 32;
c. 最后计算i的后置++或--,得i的值为9。
(2) VC下:
总体原则:先计算前两项的前置++或--,再从第三项开始从左到右一项一项的计算,最后计算所
有的后置++或--。
a. 先计算前两项i的前置++或--,i为6,前两项之和为:6 + 6 = 12;
b. 再计算第三项++i,i的值为7,前三项之和为:6 + 6 + 7 = 19;
c. 再计算第四项++i,i的值为8,前四项之和为:k = 6 + 6 + 7 + 8 = 27;
d. 最后计算i的后置++或--,得i的值为9。
2、对于直接输出表达式的值的计算,如: i = 5; printf("%d",(++i)+(i++)+(++i)+(++i));
(1) BC下:
总体原则:从左到右一项一项的计算,后置++或--在中间计算过程中计算。
a. 计算++i,得值为6,i的值为6;
b. 计算i++,得值为6,i的值为7;
c. 计算++i,得值为8,i的值为8;
d. 计算++i,得值为9,i的值为9;
所以表达式的值为:6 + 6 + 8 + 9 = 29。
(2) VC下:
计算方法同赋值计算,表达式的值为:27。
(1) BC下:
总体原则: 先计算所有的前置++或--,再计算表达式的值,最后计算后置++或--。
a. 先计算所有i的前置++或--,得出i的值为8;
b. 再计算k = 8 + 8 + 8 + 8 = 32;
c. 最后计算i的后置++或--,得i的值为9。
(2) VC下:
总体原则:先计算前两项的前置++或--,再从第三项开始从左到右一项一项的计算,最后计算所
有的后置++或--。
a. 先计算前两项i的前置++或--,i为6,前两项之和为:6 + 6 = 12;
b. 再计算第三项++i,i的值为7,前三项之和为:6 + 6 + 7 = 19;
c. 再计算第四项++i,i的值为8,前四项之和为:k = 6 + 6 + 7 + 8 = 27;
d. 最后计算i的后置++或--,得i的值为9。
2、对于直接输出表达式的值的计算,如: i = 5; printf("%d",(++i)+(i++)+(++i)+(++i));
(1) BC下:
总体原则:从左到右一项一项的计算,后置++或--在中间计算过程中计算。
a. 计算++i,得值为6,i的值为6;
b. 计算i++,得值为6,i的值为7;
c. 计算++i,得值为8,i的值为8;
d. 计算++i,得值为9,i的值为9;
所以表达式的值为:6 + 6 + 8 + 9 = 29。
(2) VC下:
计算方法同赋值计算,表达式的值为:27。
另附vc++6.0:
int x=3,y=5,z;
//
z = x*(y++)+y;
// z = 20
//
z = x+(++x)*y;
// z = 24
//
z = x+3+(++x)*y;
// z = 27
//
z = x+x+(++x)*y;
// z = 26
//
z = (++x)+x*y;
// z = 24
//
z = x*(y++)+(++x)*y;
// z = 30
//
z = (++x)+(++x)+(++x);
// z = 16
//
z = (x++)+(x++)+(x++);
// z = 9
总结:有副作用的表达式与编译器相关,应尽量少用。如面笔试题出现此类问题,可大胆填上:
不是我不会,是题没答案;
参考:
细说C/C++中的表达式运算顺序与求值顺序
C语言运算符优先级和结合性与表达式求值顺序
http://hi.chinaunix.net/?uid-20792635-action-viewspace-itemid-32448
csdn论坛:http://topic.youkuaiyun.com/u/20090516/10/f27bd123-d68f-4406-bec5-60d83ce49ffa.html