细说C/C++中的表达式运算顺序与求值顺序
来源:互联网 发布:java linux服务器 下载 编辑:程序博客网 时间:2024/05/16 06:40
1 案例描述
- int k=0;
- printf( "data : %02X %02X %02X %02X %02X %02X \n",
- dataBuf[k++], dataBuf[k++], dataBuf[k++], dataBuf[k++], dataBuf[k++], dataBuf[k++] );
data : 00 00 00 00 00 00
00 00 00 01 21 E6
2 运算顺序( Order Of Operations )
2.1 定义
2.2 C/C++中的运算顺序
3 求值顺序( Evaluation Order )
3.1 求值顺序 VS运算顺序
- main()
- {
- int a=3,b=5,c;
- c=a*b+++b;
- printf ( “c=%d”, c);
- }
c = ( a * ( b++ ) ) + b ;
f(x) = a; g(x) = b++; h(x) = b; 即 c = f(x) * g(x) + h(x);
3.2 C/C++中的求值顺序
3.2.1 C/C++标准
Except where noted, the order of evaluation of operands of individual operators and sub-expressions of individual expressions, and the order in whichside effects take place, is unspecified.
除非是已经说明的情况,表达式求值过程中的操作数和子表达式求值顺序以及副作用发生的顺序都是未说明的。
At certain specified points in the execution sequence calledsequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place.
有一些特别规定的位置叫做序列点,在序列点前的所有求值的副作用都必须完成,之后子表达式求值的副作用还没有发生。
Between the previous and nextsequence point a scalar object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be accessed only to determine the value to be stored. The requirements of this paragraph shall be met for each allowable ordering of the sub-expressions of a full expression; otherwise the behavior is undefined.
在两个相邻的序列点之间,一个对象最多只允许它储存的值被修改一次。并且访问这个对象的初始值的唯一目的只能是确定新值。子表达式的任意执行顺序都必须满足这个要求,否则代码的行为将是未定义的。
我们再看看之前的代码,“c = ( a * ( b++ ) ) + b ;” 。根据C/C++规定的少得可怜的几个序列点标准来看,整行代码只有最后的“;”结束是一个序列点。前一个序列点是上一行的分号,整行代码都处于两个序列点之间。其中的变量b在b++的时候被修改一次,为求取b++的值,我们要访问b一次,符合标准的限定。后面 +b 的时候又访问一次,便不符合标准的限定了,因此这段代码的行为是未定义的。
3.2.2 副作用(Side Effect)
C99 ( WG14 N1124 5.1.2.3 ):
Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.
C++2003:Accessing an object designated by a volatile lvalue, modifying an object, calling a library I/O function, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.
- int k = 12 + 1; // L1
- volatile int i = 1;
- k + 2; // L2
- while(k){ break; }; // L3
- while( i ) {}; // L4
3.2.3 序列点(Sequence Point)
The following are the sequence points described in 5.1.2.3:
— The call to a function, after the arguments have been evaluated (6.5.2.2).
— The end of the first operand of the following operators: logical AND && (6.5.13);
logical OR || (6.5.14); conditional ? (6.5.15); comma , (6.5.17).
— The end of a full declarator: declarators (6.7.5);
— The end of a full expression: an initializer (6.7.8); the expression in an expression statement (6.8.3); the controlling expression of a selection statement (if or switch) (6.8.4); the controlling expression of a while or do statement (6.8.5); each of the expressions of a for statement (6.8.5.3); the expression in a return statement (6.8.6.4).
— Immediately before a library function returns (7.1.4).
— After the actions associated with each formatted input/output function conversion specifier (7.19.6, 7.24.2).
— Immediately before and immediately after each call to a comparison function, and also between any call to a comparison function and any movement of the objects passed as arguments to that call (7.20.5)..
3.3 为什么
- int a=3,b=5;
- int c,d,e;
- c=a*b+++b;
- printf("c=%d\n", c);
- b=5;
- d=a*b+(++b);
- printf( "d=%d\n", d );
- b=5;
- e=a*++b+b;
- printf( "e=%d\n", e );
- int a = 3, b = 5;
- int c, d, e;
- c = a * b++ + b;
- System.Console.WriteLine("c=" + c);
- b = 5;
- d = a * b + (++b);
- System.Console.WriteLine("d=" + d);
- b = 5;
- e = a * ++b + b;
- System.Console.WriteLine("e=" + e );
VC9 WinCE
GCC 3.4.4 Win32
C# Win32
[1] Java的求值顺序参见http://java.sun.com/docs/books/jls/second_edition/html/expressions.doc.html#4779 的“15.7 Evaluation Order”
[1] C#的求值顺序定义参加http://msdn.microsoft.com/en-us/library/aa691322.aspx的“7.2 Operators”3.4 编译器都做了什么
- int c,d,e;
- c=a*b+++b;
- 00013144 ldr r1, [sp]
- 00013148 ldr r3, b, #4
- 0001314C mul r2, r1, r3
- 00013150 ldr r3, b, #4
- 00013154 add r3, r2, r3
- 00013158 str r3, c, #0xC
- 0001315C ldr r3, b, #4
- 00013160 add r3, r3, #1
- 00013164 str r3, b, #4
- printf("c=%d\n", c);
- int c, d, e;
- c = a * b++ + b;
- 00000063 mov eax,dword ptr [ebp-44h]
- 00000066 mov dword ptr [ebp-54h],eax
- 00000069 inc dword ptr [ebp-44h]
- 0000006c mov eax,dword ptr [ebp-54h]
- 0000006f imul eax,dword ptr [ebp-40h]
- 00000073 add eax,dword ptr [ebp-44h]
- 00000076 mov dword ptr [ebp-48h],eax
- System.Console.WriteLine("c=" + c);
C#先对b(ptr [ebp-44h])进行自增操作,并且提前缓存了b的旧值(在ptr [ebp-54h])用以计算a*b++的值。对比C与C#的实现可以看到,C的实现不需要缓存b的旧值,3个变量各自在3个寄存器中完成运算,这样就节约了寄存器或内存,且少了缓存b的旧值时需要的读写操作,保证了最高的效率。当然这同时导致了C和C#得到的结果不一样,虽然它们都是符合各自标准的。当然,C编译器也可以像C#这样实现,得到和C#一样的结果,同样也是符合C标准的,不符合标准的是这段代码
1 打印问题的分析
回头看看开始那段我随手写出的诡异代码:
- int k=0;
- printf( "data : %02X %02X %02X %02X %02X %02X \n",
- dataBuf[k++], dataBuf[k++], dataBuf[k++], dataBuf[k++], dataBuf[k++], dataBuf[k++] );
首先,C/C++标准中指出函数入口处有一个序列点,但它只是说函数调用(汇编中的call指令)执行前完成所有求值和副作用,但作为函数参数的表达式的求值顺序是未定义的。实际上,看汇编代码我们发现,对于这种“问题代码”编译器的处理并不符合标准,k的自增运算是函数调用完成之后才执行的。只能说编译器默认我们写出的是符合标准的代码,所以把我们的代码当作“合标代码”来进行了优化处理。如果将k的定义改一下,改成静态变量 “static int k=0; ”,那么编译器就会改在函数调用之前对k做自增处理,因为静态变量的有效区域是整个程序,编译器不能保证调用的函数里面(跨cpp的时候,函数实现对编译器来说是未知的)没有用到这个k。
还有就是这里大家不要把求值顺序和压栈顺序搞混,压栈顺序是可以确定的,默认的__cdecl调用方式参数是自右向左压栈的,但这并不代表作为参数的表达式是自右向左求值的,它们求值顺序仍然只由序列点决定。
最后,作为运算符的逗号“,“处存在一个序列点,但这里“,”是用于分隔函数参数的分隔符。
综上可知,这行打印代码有两个序列点,一个在printf函数调用,一个在此行结尾。在printf调用之前的各个参数表达式的求值顺序是完全任由编译器随意发挥的。反汇编后我们可以知道VC是先取值压栈然后对k做6次自增,最后调用printf函数的。
如下:
- 14: printf( "data : %02X %02X %02X %02X %02X %02X \n",
- dataBuf[k++], dataBuf[k++], dataBuf[k++], dataBuf[k++], dataBuf[k++], dataBuf[k++] );
- 0040F36B mov eax,dword ptr [ebp-0Ch]
- 0040F36E movsx ecx,byte ptr [ebp+eax-8]
- 0040F373 mov dword ptr [ebp-10h],ecx
- 0040F376 mov edx,dword ptr [ebp-10h]
- 0040F379 push edx
- 0040F37A mov eax,dword ptr [ebp-0Ch]
- 0040F37D movsx ecx,byte ptr [ebp+eax-8]
- 0040F382 mov dword ptr [ebp-14h],ecx
- 0040F385 mov edx,dword ptr [ebp-14h]
- 0040F388 push edx
- 0040F389 mov eax,dword ptr [ebp-0Ch]
- 0040F38C movsx ecx,byte ptr [ebp+eax-8]
- 0040F391 mov dword ptr [ebp-18h],ecx
- 0040F394 mov edx,dword ptr [ebp-18h]
- 0040F397 push edx
- 0040F398 mov eax,dword ptr [ebp-0Ch]
- 0040F39B movsx ecx,byte ptr [ebp+eax-8]
- 0040F3A0 mov dword ptr [ebp-1Ch],ecx
- 0040F3A3 mov edx,dword ptr [ebp-1Ch]
- 0040F3A6 push edx
- 0040F3A7 mov eax,dword ptr [ebp-0Ch]
- 0040F3AA movsx ecx,byte ptr [ebp+eax-8]
- 0040F3AF mov dword ptr [ebp-20h],ecx
- 0040F3B2 mov edx,dword ptr [ebp-20h]
- 0040F3B5 push edx
- 0040F3B6 mov eax,dword ptr [ebp-0Ch]
- 0040F3B9 movsx ecx,byte ptr [ebp+eax-8]
- 0040F3BE mov dword ptr [ebp-24h],ecx
- 0040F3C1 mov edx,dword ptr [ebp-24h]
- 0040F3C4 push edx
- 0040F3C5 push offset string "data : %02X %02X %02X %02X %02X "... (00422fac)
- 0040F3CA mov eax,dword ptr [ebp-0Ch]
- 0040F3CD add eax,1
- 0040F3D0 mov dword ptr [ebp-0Ch],eax
- 0040F3D3 mov ecx,dword ptr [ebp-0Ch]
- 0040F3D6 add ecx,1
- 0040F3D9 mov dword ptr [ebp-0Ch],ecx
- 0040F3DC mov edx,dword ptr [ebp-0Ch]
- 0040F3DF add edx,1
- 0040F3E2 mov dword ptr [ebp-0Ch],edx
- 0040F3E5 mov eax,dword ptr [ebp-0Ch]
- 0040F3E8 add eax,1
- 0040F3EB mov dword ptr [ebp-0Ch],eax
- 0040F3EE mov ecx,dword ptr [ebp-0Ch]
- 0040F3F1 add ecx,1
- 0040F3F4 mov dword ptr [ebp-0Ch],ecx
- 0040F3F7 mov edx,dword ptr [ebp-0Ch]
- 0040F3FA add edx,1
- 0040F3FD mov dword ptr [ebp-0Ch],edx
- 0040F400 call printf (00401060)
- 0040F405 add esp,1Ch
如果将“k++”换成“++k”在vc6上编译的结果更诡异,Debug和Release的结果是不同的。大家有兴趣可以试一下。
2 今天你PC-lint了没有
不管是VC6,VC9,GCC等都不会对这样的代码做任何警告或提示,它们都高高兴兴的编译通过,默默无闻的为我们做优化,而且因为狗屎运的存在,很多时候程序执行结果和我们期望的一样,只不过这些问题代码就像埋下了一颗颗地雷,当换了编译器,或改了编译选项就可能不幸引爆,出现诡异的bug,所谓“Shit happens”。到时候又会有人大喊:“怎么回事?怎么换了版本就不行了?”;“我发誓,这程序我一行代码也没动过!”。
好消息是PC-lint可以检查出这些错误,上面的示例代码都会被PC-lint给以错误警告:
printf( "data : %02X %02X %02X %02X %02X %02X \n", dataBuf[++k], dataBuf[++k], dataBuf[++k], dataBuf[++k], dataBuf[++k], dataBuf[++k] );
E:\study\testvc6\testvc6.cpp(14): error 564: (Warning -- variable 'k' depends on order of evaluation)
E:\study\testvc6\testvc6.cpp(14): error 564: (Warning -- variable 'k' depends on order of evaluation)
E:\study\testvc6\testvc6.cpp(14): error 564: (Warning -- variable 'k' depends on order of evaluation)
E:\study\testvc6\testvc6.cpp(14): error 564: (Warning -- variable 'k' depends on order of evaluation)
E:\study\testvc6\testvc6.cpp(14): error 564: (Warning -- variable 'k' depends on order of evaluation)
c=a*b+++b;
E:\study\testvc6\testvc6.cpp(17): error 564: (Warning -- variable 'b' depends on order of evaluation)
- 细说C/C++中的表达式运算顺序与求值顺序
- C/C++ 语言中的表达式求值顺序
- c语言表达式求值顺序
- C,C++表达式求值顺序
- C,C++表达式求值顺序
- C,C++表达式求值顺序
- C,C++表达式求值顺序
- C,C++表达式求值顺序
- C,C++表达式求值顺序
- C语言表达式求值顺序
- C,C++表达式求值顺序
- C,C++表达式求值顺序
- C,C++表达式求值顺序
- C/C++表达式求值顺序
- C,C++表达式求值顺序
- C/C++表达式求值顺序
- C,C++表达式求值顺序
- c语言中的求值顺序
- Self-Study Machine Learning Projects
- 第三周作业-循环与判断语句(网络131 梁文俊
- C#;Cookies;例
- 怎样才能持之以恒
- Cmake 设置交叉编译环境
- 细说C/C++中的表达式运算顺序与求值顺序
- 机器学习中的相似性度量
- >/dev/null2>&1
- java 环境配置
- 设置AlertDialog的大小位置
- 2001年清华大学计算机研究生机试真题(第I套)之一
- 神经网络训练中的训练集、验证集以及测试集合
- 设置二进制
- JDom,jdom解析xml文件