王朝百科
分享
 
 
 

求值顺序

王朝百科·作者佚名  2010-04-18  
宽屏版  字体: |||超大  

关于C/C++ 表达式求值顺序表达式求值顺序不同于运算结合性和优先级。下面是一个经典例子,被 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<int> );

void f_2( auto_ptr<int>, int );

f( auto_ptr<int>( new int ) ); // exception save

f( auto_ptr<int>( 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 是一个裸指针,不会有任何机制帮助它释放其内存!

 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
如何用java替换看不见的字符比如零宽空格&#8203;十六进制U+200B
 干货   2023-09-10
网页字号不能单数吗,网页字体大小为什么一般都是偶数
 干货   2023-09-06
java.lang.ArrayIndexOutOfBoundsException: 4096
 干货   2023-09-06
Noto Sans CJK SC字体下载地址
 干货   2023-08-30
window.navigator和navigator的区别是什么?
 干货   2023-08-23
js获取referer、useragent、浏览器语言
 干货   2023-08-23
oscache遇到404时会不会缓存?
 干货   2023-08-23
linux下用rm -rf *删除大量文件太慢怎么解决?
 干货   2023-08-08
刀郎新歌破世界纪录!
 娱乐   2023-08-01
js实现放大缩小页面
 干货   2023-07-31
生成式人工智能服务管理暂行办法
 百态   2023-07-31
英语学习:过去完成时The Past Perfect Tense举例说明
 干货   2023-07-31
Mysql常用sql命令语句整理
 干货   2023-07-30
科学家复活了46000年前的虫子
 探索   2023-07-29
英语学习:过去进行时The Past Continuous Tense举例说明
 干货   2023-07-28
meta name="applicable-device"告知页面适合哪种终端设备:PC端、移动端还是自适应
 干货   2023-07-28
只用css如何实现打字机特效?
 百态   2023-07-15
css怎么实现上下滚动
 干货   2023-06-28
canvas怎么画一个三角形?
 干货   2023-06-28
canvas怎么画一个椭圆形?
 干货   2023-06-28
canvas怎么画一个圆形?
 干货   2023-06-28
canvas怎么画一个正方形?
 干货   2023-06-28
中国河南省郑州市金水区蜘蛛爬虫ip大全
 干货   2023-06-22
javascript简易动态时间代码
 干货   2023-06-20
感谢员工的付出和激励的话怎么说?
 干货   2023-06-18
 
>>返回首页<<
 
 
 
静静地坐在废墟上,四周的荒凉一望无际,忽然觉得,凄凉也很美
© 2005- 王朝网络 版权所有