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 = 0,前者令 i 变成1, 后者令 i变成2。由于没有求值顺序点,语言不保证这两个副作用的顺序。 所以最终 i 的值可能是1 ,也可能是2。

 

   下面的术语以 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的右侧运算结束后并没有求值顺序点。

    在两个求值顺序点之间,子表达式求值和副作用的顺序是不确定的。 可以看到,所有的赋值运算、自增自减都不包括在内,所以一旦某个表达式修改同一个变量超过一次,他的结果就是不确定的:

n = n++;                               // 两个副作用,顺序不确定

    同时,几乎所有表达式,求值顺序都不确定。比如,下面的加法, f1 f2 f3的调用顺序是任意的:

n = f1() + f2() + f3();            // f1 f2 f3 调用顺序任意

    而函数也只在实际调用前有一个求值顺序点。所以,常见于早期 C 语言教材的这类题目,是错题:

printf("%d",--a+b,--b+a);    // --a + b 和 --b + a 这两个子表达式,求值顺序不确定

 

 

   再看看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 的求值、以及 ++i 副作用的顺序是任意的。这会引起混乱。

    更可怕的是,重载 && 和 || 。 大家已经习惯了其速死算法: 如果左侧求值已经决定了最终结果,则右侧不会被求值。而且大家很依赖这个行为,比如是C风格字符串拷贝常常这样写:

while( p && *p )
    *pd++ = *p++;

    假如p 为 0, 那么 *p 的行为是未定义的,可能令程序崩溃。 而 && 的求值顺序避免了这一点。 但是,如果我们重载 && 就等于下面的做法:

exp1 .operator && ( exp2 )

    现在不仅仅是求值混乱了。无论exp1是什么结果,exp2 必然会被求值。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值