【C语言】序列点和副作用

C标准规定:在两个序列点之间,一个对象所保存的值最多只能被修改一次。

C标准规定:在两个序列点之间,副作用的执行顺序是不确定的。

C标准规定:两个序列点之间的执行顺序是任意的。当然这个任意是在不违背操作符优先级和结合特性的前提下的,这个规定的意义是为编译器的优化留下空间。

一、序列点和副作用

副作用(side effect) 是对数据对象或文件的修改。例如:
states = 50; //它的副作用是将变量的值设置为50。

为什么叫副作用?
因为从C语言的角度看, 主要目的是对表达式求值。给出表达式4 + 6,C会对其求值得10;给出表达式states = 50,C会对其求值得50,对该表达式求值的副作用是把变量states的值改为50。

具有副作用的运算符:赋值运算符递增运算符以及递减运算符

序列点(sequence point) 是程序执行的点,在该点上,所有的副作用都在进入下一步之前发生。

虽然副作用是在进入下一步之前发生,但是下一步之前还有很多步,这个时间点是不明确的。所以对于y = i++ + ++i; 这种语句会多次存取变量i,2次自增,C未指明什么时候存取,什么时候自增,这就造成结果的不确定性,所以这种代码要避免。

在C语言中,有三类标记序列点:
1、语句中的分号标记了一个序列点;
2、一些运算符也有序列点,逗号运算符、逻辑与运算符以及逻辑或运算符,条件运算符。
3、任何一个完整表达式的结束也是一个序列点。

二、举例说明

下面就是标准中定义的序列点:
(1)函数调用时,实参表内全部参数求值结束,函数的第一条指令执行之前(注意参数分隔符“,”不是序列点);
(2)&&操作符的左操作数结尾处;
(3)||操作符的左操作数结尾处;
(4)?:操作符的第一个操作数的结尾处;
(5)逗号运算符;
(6)表达式求值的结束点,具体包括下列几类:自动对象的初值计算结束处;表达式语句末尾的分号处; do/while/if/switch/for语句的控制条件的右括号处;for语句控制条件中的两个分号处;return语句返回值计算结束(末尾的分号)处。

定义序列点是为了尽量消除编译器解释表达式时的歧义,如果序列点还是不能解决某些歧义,那么标准允许编译器的实现自由选择解释方式。

2.1 &&和||

因为&&支持短路操作,必须先将&&左边的表达式计算完毕(副作用生效),如果结果为false,则不必再计算&&右边的表达式,直接返回false。
||和&&类似,如果||左边为true,则不必再计算||右边的表达式,直接返回true。

有一种短路算法来解决除法中的除0情况。

int a = 10;
int b = 0;
if (b && a/b) 
{ /* some code here */ }

在求b的值的时候会短路,即,a/b不会执行。因为b的值为0,这样
可以放心的使用除法了。

2.2 ?:

三目运算符 ?:中的"?"会产生序列点。 如,

int a = 5;
int b = a++ > 5 ? 0 : a;

条件表达式先计算a++ > 5,为false,又因为?会产生序列点,所以副作用生效,a等于6,既然是flase,则条件运算符最后的结果等于a,即6。

2.3 逗号运算符

int b = 5, ++b;

逗号运算符保证了被它分隔的表达式从左往右求值(这个从左到右的顺序不是结合律的从左到右的含义)(换言之, 逗号是一个序列点, 所以逗号左侧项的所有副作用都在程序执行逗号右侧项之前发生),最后的值为6。
顺带说一下,整个逗号表达式的值是右侧项的值(y = (x++, 2),若x的值为0,y的值为2)。

这里需要说明一下,对于printf(a++, b++);这种函数内实参之间的逗号不是逗号运算符,而是一个分隔符,所以先就算a++还是b++存在随机性且副作用不会生效。

2.4 完整表达式

完整表达式(full expression),就是指这个表达式不是另一个更大表达式的子表达式。 例如, 表达式语句中的表达式和while循环中的作为测试条件的表达式, 都是完整表达式。

while(a++ < 10)
	printf("%d\n", a);

对于该例,因为while后面没有分号,一部分人认为“先使用值, 再递增它”的意思是,在printf()语句中先使用a, 再递增它。
表达式a++ < 10是一个完整的表达式,因为它是while循环的测试条件, 所以该表达式的结束就是一个序列点。因此,C 保证了在程序转至执行printf()之前发生副作用(即,递增a) 。 同时, 使用后缀形式保证了a在完成与10的比较后才进行递增。

三、个人理解

程序通过序列点将程序分成若干句,程序按序列点的先后顺序,按序依次执行。在两个序列点之间的语句,通过优先级和结合特性,使得程序按一定的顺序去执行。编译器按照C语言的规定进行编译汇编等工作,将C语言转变成汇编代码。当然编译器在工作的过程中也会遇到问题,比如:z = fun1()+fun2()+fun3(),两个加号优先级一样,则按结合性,从左往右结合。但是这时候只是规定了计算的顺序,对于函数的求值顺序是未定义的。C语言中没有规定先求fun1,再求fun2,再两个值相加,再求fun3,再相加。我们可以先求函数fun3的值,再求函数2,再求函数1的值,最后按计算顺序相加。表达式求值的顺序和运算符的优先级和结合性没有一点关系。

如果三个函数都调用一个全局变量并对全局变量进行赋值,那么不同的调用顺序很可能导致错误的结果。

相似的例子还有:

int i = 1;
i = i++ + i; 

讲这个例子之前,先说两问题
1、i = 3+a有几步

int i = 2;
i = 3;

这种语句查看其汇编代码为:
在这里插入图片描述
只有1步。

int i = 2;
int a = 1;
i = 3+a;

这种语句查看其汇编代码为:
在这里插入图片描述
所以对于i=3+a,这种语句总共有两步首先求表达式的值3+a = 4,第二步将表达式的值写到内存i中,即副作用。

2、++有几步

int i = 1;
i++;
++i;

i++语句作为子表达式时,首先求表达式的值为1,附带一个自增副作用;
++i语句作为子表达式时,首先求表达式的值为2,附带一个自增副作用。

i++ 和 ++i 是一样的,都不知道自增副作用什么时候生效。

回到上一个例子(i = i++ + i) :共4种情况。
情况一、
在这里插入图片描述
情况二、
在这里插入图片描述
情况三、
在这里插入图片描述
情况四、
在这里插入图片描述
副作用出现的时间,是在具有副作用运算符运算之后的任意时间,当然是在序列点前的。

所以,由于副作用出现的不确定性,两个序列点之间,出现多个同名的变量,不要对其中一个或多个变量进行赋值操作。

四、参考资料

[1]: 谈谈C语言中的序列点(sequence point)和副作用(side effects)
[2]: C Primer Plus 第六版

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值