C语言的黑暗角落: 副作用与序列点
时间:2010-06-21 来源:slimzhao
副作用与序列点
int a = i++;变量a取得i在自增1之前的值, 表达式i++的正作用是产生i的值(左值), 副作用是要保证i自增1. 地球人都知道. 下面是地球人不一定都知道的:
mov eax, [esp-12] ; 取变量i的值第三条语句与第二条语句互换也是完全可以的. 这在一条简单语句里没什么稀奇;
mov [esp-16], eax ; 将取得的变量i的值存入变量a
inc [esp-12] ; 让变量i自增1
int i = 1;即使我没有写很变态的 i+++++i, 即使我多余地加了括号以正视听, 这个语句的值还是暗藏玄机, 问题在于它不止有一个符合C语言标准的值. 它可以是2, 3. 因为语言标准对这种情况副作用于何时发生未作规定, 编译器可以任意决定.
int a = (i++) + (i++);
mov eax, [esp-12] ; 取变量i的值这样得到3.
inc [esp-12] ; 让变量i自增1
mov ebx, [esp-12] ; 取变量i的值
inc [esp-12] ; 让变量i自增1
add eax, ebx ; 相加
mov [esp-16], eax ; 结果存入a
mov eax, [esp-12] ; 取变量i的值这样得到2.
mov ebx, [esp-12] ; 取变量i的值
inc [esp-12] ; 让变量i自增1
inc [esp-12] ; 让变量i自增1
add eax, ebx ; 相加
mov [esp-16], eax ; 结果存入a
C99标准中这样说:
2 Accessing a volatile object, modifying an object, modifying a file, or calling a function
that does any of those operations are all side effects,11) which are changes in the state of
the execution environment. Evaluation of an expression may produce side effects. At
certain specified points in the execution sequence called sequence points, all side effects
of previous evaluations shall be complete and no side effects of subsequent evaluations
shall have taken place. (A summary of the sequence points is given in annex C.)
对sequence point的定义是:
Sequence points为什么叫序列点, 我猜想这个术语的选择是基于这样的考虑: 底层最终负责执行的机器(或C假想中的一个C语言执行机)需要以更原始的操作来实现C语言中的一条语句. 这些操作当然与C的高级语句不一定是一一对应的关系, 所以需要确定在这些原始操作操作中的一些特殊的点, 当执行流到这样的特殊点时, 恰好对应一个C语言语句或表达式完成了它的全部语意(包括副作用). 在这些点上C语言的表达式是意义完整的. 如 int a = i++;
1 The following are the sequence points described in 5.1.2.3:
--- The call to a function, after the arguments have been evaluated (6.5.2.2).
--- The end of the first operand of the following operators: logical AND && (6.5.13);
logical OR || (6.5.14); conditional ? (6.5.15); comma , (6.5.17).
--- The end of a full declarator: declarators (6.7.5);
--- The end of a full expression: an initializer (6.7.8); the expression in an expression
statement (6.8.3); the controlling expression of a selection statement (if or switch)
(6.8.4); the controlling expression of a while or do statement (6.8.5); each of the
expressions of a for statement (6.8.5.3); the expression in a return statement
(6.8.6.4).
--- Immediately before a library function returns (7.1.4).
--- After the actions associated with each formatted input/output function conversion
specifier (7.19.6, 7.24.2).
--- Immediately before and immediately after each call to a comparison function, and
also between any call to a comparison function and any movement of the objects
passed as arguments to that call (7.20.5).
mov eax, [esp-12] ; 取变量i的值执行点不能是在第1或第2条语句之后, 因为此时无法确定i++的状态.
mov [esp-16], eax ; 将取得的变量i的值存入变量a
inc [esp-12] ; 让变量i自增1
对序列点的精确定义确定了在什么样的范围内同一个对象的副作用发生多次时其结果是标准未加规定的, 曾经看到一个叫"时代兔子"的在
http://www.gdglc.com/bbs/TopicOther.asp?t=5&BoardID=23&id=345说: 标准规定,在两个序列点之间,一个对象所保存值最多只能被修改一次。
对这个"只能被修改一次", 可以做下面的理解:
- 如果通过副作用在两个序列点之间修改了同一个对象两次, 程序执行时只会修改它一次.
- 程序员只能修 改一次, 修改多次时(1)编译时会报错(2)运行时会怎样?
Between two sequence points, an object is modified more than once, or is modified包括了这样的情况
and the prior value is read other than to determine the value to be stored (6.5).
a = i + i++;上面的意思是在两个序列点之间, 一个对象被修改多于一次, 或者(虽然只被修改一次)被修改的同时还被读取了, 而且这个读取并非用于修改该对象. 上面i++的实现如果是通过(1)读取i的值到寄存器中(2)将寄存器中的值加1(3)将寄存器中的值存回变量i所在的存储单元实现的, 则步骤(1)中的读取就是"用于修改该对象"的读取. 标准中的那句话即指如果在两个序列点之间, 除了这次读取还有其它的读取, 那么即使只修改对象一次, 其行为也是未定义的. 在i + i++中, 作为+号运算符的第一个运算子i值的获取就需要一次"读", 而这次读不是用于修改i值的那次.
我相信这一点对于即使是了解序列点概念的人来说, 比起相邻两个序列点之间对同一对象的多于一次修改更为阴险.
注意上面的(1)(2)(3)步骤实现i++是完全可能的, 这是因为并非所有的机器指令集都如Intel的那样支持对一个内存单元的内容直接增1, 很有可能对内存单元的任何修改都必需通过寄存器(比如典型的RISC指令集 MIPS)
相关阅读 更多 +