《你的月亮我的C》(五):表达式的求值顺序

  • arr[ i ]=i++;有的编译器可以允许,有的编译器会报错,为什么?

因为i这个变量在同一个表达式里被两个地方引用了。表达式右边i++做后缀自增行为,但是在表达式左边arr[ i ]中无法判断应用引用i的旧值还是新值。有的书本认为这种表达式是行为不确定的。按照C标准这种表达式是未定义的。所以有的编译器会报错。当然你也有可能得到这样的结果:

  • int i=5; printf("%d\n", i++ * i++); 有的编译器得到30,有的编译器得到25?

虽然后缀自增(或后缀自减)操作符会在返回旧值之后再做自增(自减)运算,但在表达式中,(不同的编译器)无法保证变量的自增(自减)会在返回旧值之后或对表达式的其他部分进行运算之前进行。也就是说无法确定变量的运算会在表达式执行完之前的某一个“序列点”进行。如果你的编译器得到的结果是25,则说明编译器是先把变量的旧值相乘以后再对两者进行自增运算。如果编译器得到的结果是30,则说明先对i返回数值5,然后自增得到i等于6,再对二者进行相乘5 x 6 = 30。

  • 那么可不可以通过添加显式括号来控制计算机对表达式的运算顺序?例如:arr[ i ]=(i++)。

一般来说不要写这样的“复杂”不便于清晰明了的代码。而且,显式括号(和操作符优先级)对表达式的运算顺序只是有部分影响(这里的“部分”不包括对操作数的求值),例如对函数调用的操作

FinMid() + FinMax() * FinMin();

虽然乘号的运算优先级大于加号,但是不一定FinMax() 和 FinMin()这两个函数就会优先被调用。如果你给它们加上一对显式括号:

FinMid() +( FinMax() * FinMin() );

这样做只是告诉编译器这两个函数结合一起,但并不表示要求编译器先对括号内的表达式进行运算。

  • 那么为什么while((c=getchar())!=EOF && c!=\n)这样的代码可以用显式括号来控制表达式的顺序?

&& 、||和?:操作符都会在表达式中引入一个额外的序列点,所以才有&&和||操作符“短路”原因,即对于&&操作符,左边为假的话,则右边的子表达式不会计算,对于||操作符,左边为真的话,右边的子表达式也不会计算。因此,在有&& 、||和?:操作符的表达式中,可以确保从左到右的顺序,所以可以通过显式括号来控制表达式的顺序。

 

  • 最后来聊一聊“序列点”是什么。

序列点是一个时间点。存在于:

  1. 完整表达式的尾部;
  2. “&&” 、“||” 、“?:”和“,”操作符处;
  3. 函数调用时;

 

ANSI/ISO C标准中有一段话很重要:“在上一个序列点和下一个序列点之间,一个对象所保存的值最多只能被表达式的求值修改一次。而且只有在确定将要保存的值的时候才能访问前一个值。”

(在完整表达式语句中,上一个序列点通常指上一条语句的结束分号,而下一个序列点通常指当前语句的结束分号。)

按照标准中的那句“在上一个序列点和下一个序列点之间,一个对象所保存的值最多只能被表达式的求值修改一次。”,所以上面第二点提到的printf("%d\n", i++ * i++);中i++ * i++或者i=i++这样的代码中,i的值在两个序列点之间被修改了两次,所以这样的代码是“不允许”的。

同样,“而且只有在确定将要保存的值的时候才能访问前一个值。”这句话也否定了上面第一点中的arr[ i ]=i++;这样的代码,因为左式子arr[ i ]中的i,不确定是右式中i的旧值还是i自增后的新值。

有的人会发现,arr[ i ]=i++; 、i=i++; 这些语句,在编译器上可以通过编译和运行,且得到了我想要的结果。对于上面提到的那些不确定行为,编译器可能会通过编译并得到一个结果,但这个结果哪怕是你所期望的,但不一定是正确的结果。就好像医生告诉我感冒药不能乱吃,但是我没事吃了颗感冒药后没有任何不适,所以我说:“其实没事也可以吃感冒药嘛。”

总的来说,对于没有好的办法来决定运算应该在变量的自增自减前还是后这样的情况,我们就不要写这样的代码,因为可移植程序中不应该用这样不确定的语句。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值