C语言Side Effect与Sequence Point
2016-07-16 17:03
218 查看
转自:http://docs.linuxtone.org/ebooks/C&CPP/c/ch16s03.html
Side Effect与Sequence Point
如果你只想规规矩矩地写代码,那么基本用不着看这一节。本节的内容基本上是钻牛角尖儿的,除了Short-circuit比较实用,其它写法都应该避免使用。但没办法,有时候不是你想钻牛角尖儿,而是有人逼你去钻牛角尖儿。这是我们的学员在找工作笔试时碰到的问题:int a=0; a = (++a)+(++a)+(++a)+(++a);
据我了解,似乎很多公司都有出这种笔试题的恶趣味。答案应该是Undefined,我甚至有些怀疑出题的人是否真的知道答案。下面我来解释为什么是Undefined。
我们知道,调用一个函数可能产生Side Effect,使用某些运算符(++、--、=、复合赋值)也会产生Side Effect,如果一个表达式中隐含着多个Side Effect,究竟哪个先发生哪个后发生呢?C标准规定代码执行过程中的某些时刻是Sequence Point,当到达一个Sequence
Point时,在此之前的Side Effect必须全部作用完毕,在此之后的Side Effect必须一个都没发生。至于两个Sequence Point之间的多个Side Effect哪个先发生哪个后发生则没有规定,编译器可以任意选择各Side Effect的作用顺序。下面详细解释各种Sequence Point(出自[C99]的Annex
C)。
1、调用一个函数时,在所有准备工作做完之后、函数调用开始之前是Sequence Point。比如调用
foo(f(), g())时,
foo、
f()、
g()这三个表达式哪个先求值哪个后求值是Unspecified,但是必须都求值完了才能做最后的函数调用,所以
f()和
g()的Side
Effect按什么顺序发生不一定,但必定在这些Side Effect全部作用完之后才开始调用
foo函数。
2、条件表达式?:、逗号运算符,、逻辑与&&、逻辑或||的第一个操作数求值之后是Sequence Point。我们刚讲过条件表达式和逗号运算符,条件表达式要根据表达式1的值是否为真决定下一步求表达式2还是表达式3的值,如果决定求表达式2的值,表达式3就不会被求值了,反之也一样,逗号运算符也是这样,表达式1求值结束才继续求表达式2的值。
逻辑与和逻辑或早在“布尔代数”一节就讲了,但在初学阶段我一直回避它们的操作数求值顺序的问题。这两个运算符和条件表达式类似,先求左操作数的值,然后根据这个值是否为真,右操作数可能被求值,也可能不被求值。比如例 8.3
“剪刀石头布”这个程序中的这几句:
ret = scanf("%d", &man); if (ret != 1 || man < 0 || man > 2) { printf("Invalid input! Please input 0, 1 or 2.\n"); continue; }
其实可以写得更简单([K&R]书上的代码风格就是这样):
if (scanf("%d", &man) != 1 || man < 0 || man > 2) { printf("Invalid input! Please input 0, 1 or 2.\n"); continue; }
这个控制表达式的求值顺序是:先求
scanf("%d", &man) = 1的值,如果
scanf调用失败,则返回值不等于1成立,||运算有一个操作数为真则整个表达式为真,这时直接执行下一句
printf,根本不会再去求
man < 0或
man > 2的值;如果
scanf调用成功,则读入的数保存在变量
man中,并且返回值等于1,那么说它不等于1就不成立了,第一个||运算的左操作数为假,就会去求右操作数
man < 0的值作为整个表达式的值,这时变量
man的值正是
scanf读上来的值,我们判断它是否在[0,
2]之间,如果
man < 0不成立,则整个表达式
scanf("%d", &man) != 1 || man < 0的值为假,也就是第二个||运算的左操作数为假,所以最后求右操作数
man > 2的值作为整个表达式的值。
&&运算与此类似,
a && b的计算过程是:首先求
a,如果
a的值是假则整个表达式的值是假,不会再去求
b;如果
a的值是真,则下一步求
b的值作为整个表达式的值。所以,
a && b相当于
if (a) b;,而
a || b相当于“if (!a) b;”。这种特性称为Short-circuit,很多人喜欢利用Short-circuit特性使代码更加简洁。
3、在一个完整的声明末尾是Sequence Point,所谓完整的声明是指这个声明不是另外一个声明的一部分。比如声明
int a[10], b[20];,在
a[10]末尾是Sequence Point,在
b[20]末尾也是。
4、在一个完整的表达式末尾是Sequence Point,所谓完整的表达式是指这个表达式不是另外一个表达式的一部分。所以如果有
f(); g();这样两条语句,
f()和
g()是两个完整的表达式,
f()的Side
Effect必定在
g()之前发生。
5、在库函数返回时是Sequence Point。这似乎可以包含在上一条规则里面,因为函数返回必然会结束掉一个表达式,开始一个新的表达式。事实上以后我们会讲到,很多库函数是以宏定义的形式实现的,并不是真的函数,所以才需要有这条规则。
6、像
printf、
scanf这种带转换说明的输入/输出库函数,在处理完每一个转换说明相关的输入/输出操作时是一个Sequence Point。
7、库函数
bsearch和
qsort在查找和排序过程中的每一步比较或移动操作之间是一个Sequence Point。
现在可以分析一下本节开头的例子了。
a = (++a)+(++a)+(++a)+(++a);的结果之所以Undefined,是因为在这个表达式中对变量
a的Side Effect有五次,这些Side Effect何时发生、按什么顺序发生是不一定的,只知道在整个表达式结束时一定都发生了,但在计算过程中要用到
a的值时,能取出什么值就不确定了。这行代码用不同平台的不同编译器来编译,结果是不同的,甚至在同一平台上用同一编译器的不同版本来编译也可能不同。
写表达式应遵循的原则一:在两个Sequence Point之间,同一个变量的值只允许被改变一次。仅有这一条原则还不够,例如
a[i++] = i;的变量
i只改变了一次,但结果仍是Undefined,因为等号左边改
i的值,等号右边读
i的值,到底是先改还是先读?这个读写顺序是不确定的。但为什么
i = i + 1;就没有歧义呢?虽然也是等号左边改
i的值,等号右边读
i的值,但你不读出
i的值就没法计算
i + 1,那拿什么去改
i的值呢?所以这个读写顺序是确定的。所以,写表达式应遵循的原则二:如果在两个Sequence
Point之间既要读一个变量的值又要改它的值,只有在读写顺序确定的情况下才可以这么写。
相关文章推荐
- 如何组织构建多文件 C 语言程序(二)
- 如何写好 C main 函数
- Lua和C语言的交互详解
- 关于C语言中参数的传值问题
- 简要对比C语言中三个用于退出进程的函数
- 深入C++中API的问题详解
- 基于C语言string函数的详解
- C语言中fchdir()函数和rewinddir()函数的使用详解
- C语言内存对齐实例详解
- C语言编程中统计输入的行数以及单词个数的方法
- C 语言简单加减乘除运算
- C语言自动生成enum值和名字映射代码
- C语言练习题:自由落体的小球简单实例
- 使用C语言判断英文字符大小写的方法
- c语言实现的带通配符匹配算法
- C语言实现顺序表基本操作汇总
- C语言中进制知识汇总
- C语言判断一个数是否是2的幂次方或4的幂次方
- C语言二进制思想以及数据的存储
- C语言中计算正弦的相关函数总结