1. 问题的提出
K&R的书中一再强调"C is not a big language",当时看书的时候无法理解这句话的意思。现在我的理解是C标准本身的限制比较小,留给程序员的空间较大。这样一来C中的而有些问题标准就没有给出限定,就会产生一些让人迷惑的地方。今天我遇到了一个这样的问题,解决这些问题有的时候看起来是“钻牛角尖”,好吧,我就是一个爱钻牛角尖的人。这里需要解决的问题是执行顺序的问题,先给出几个问题。
int i = 0, j = 0;
j = i++ + i++ + i++;
执行完这两个语句后i和j的值各是什么?(看着是不是很熟悉,上次笔试考C中是不是有这个呢)
int i=0;
int arry[10];
arry[i] = i++;
执行完上述语句后,i和数组arry中的值是多少?
#define PRINT(x, y, z) printf(" x = %d, y = %d, z = %d\n", (x), (y),(z))
x = y = z = 1;
++x || ++y && ++z; PRINT(x, y, z);
输出结果是什么?(来自于C Puzzle Book Operators 1.6 )
先把这几个问题放在这里,先思考下,下面将会解决他们。
2. 明确两个概念:side effects, sequence point
2.1 side effects
C语言经典著作 “The C Programming Language” 中对于side effects的定义:
Function calls, nested assignment statements, and increment anddecrement
operators cause ‘side effects’ – some variable is changed as a by-product
of the evaluation of an expression.
这里述说的side effect可以理解为一种“副作用”,这种作用是改变一个变量的值。
“C In Nutshell” 中关于side effects的定义:
In addition to yielding a value, the evaluation of an expression can result in other changes in the execution environment, called side effects. Examples of such changes include modifications of a variable's value, or of input or output streams.
相对于K&R中的定义这里使用了对于环境的改变,这应该更加准确。总结:side effects 就是程序中的实体产生的改变,这里所说的实体通常指变量。
赋值,自增,自减表达式会产生side effects,函数调用表达式也有可能产生side effects。
2.2 sequence points
sequence points: A sequence point is a point in time at which the dust has settled and all side effects which have been seen so far are guaranteed to be complete. The sequence points listed in the C standard are:
- at the end of the evaluation of full expression ( a full expression is an expression statement, or any other expression which is not subexpression within any large expression);
- at the ||, &&, ?:,and comma operators; and
- at a function call (after the evaluation of all arguments, and just before the actual call).
序列点(sequence points)是一种逻辑意义的点,它的意义在于,逻辑点前的副作用(side effects)都在这时生效。C标准中定义的序列点总共有三类,第一类是完全表达式(full expression);第二类是||,&&,?:和;第三类是函数调用,在所有的参数确定后、函数真正调用之前。
2.3 side effects 和sequence points对于编写程序有什么意义?
2.3.1我的“SS1”和“SS2”原则
标准中规定了在前一个序列点前的副作用都会在前一个序列点后完成,但是标准没有规定两个序列点之间的副作用生效的顺序,不同的C语言实现的顺序可能不同。请注意这一点,这是所有问题产生的根本原因。如果两个序列点之间有超过两个的副作用作用在同一个实体上,这样不同的编译器产生的结果就不同,这种情况在标准中称为unspecified 。所以在实际应用中应该避免这种情况的出现,我把这一个原则称为为SS1。
是不是遵守了SS1原则就不会产生unspecified了呢?非也。可以设想这样一种情况:每一个实体(A)在两个序列点之间被两次使用,只有一次对这个实体本身产生副作用,另外一次被间接的用来产生副作用作用于另外一个实体(B)。在前面设想的这种情况下虽然符合SS1原则,但是我们会发现被间接用来产生副作用时,对于实体(B)产生的副作用肯定会跟实体(A)有关,但是这个实体(A)在这个序列点区间中有被副作用作用,那么我们就无法确定这个实体(A)的值了,从而实体(B)也就无法确定了。这里可以归纳为:在两个序列点之间,如果出现对一个实体的多次引用,并且只有一次会对该实体产生副作用(SS1),那么所有的这些引用都必须用来产生这个副作用 ,我把这一个原则称为SS2。只有同时遵守了SS1和SS2,写出的表达式才不是unspecified类型的。
2.3.2 标准
The standard states that Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of the expression. Furthermore, the prior value shall be accessed only to determine the value to be stored.
可以清晰的看到标准中使用了两句话来概括这种问题,这正好对应于SS1,SS2原则。
2.3.3 怎样才能避免写出未定义的表达式出来?
下面给出更为具体的方法:
1)在一个表达式中最多只改变一个实体。
2)如果一个实体在一个表达式被改变,并且出现次数大于一次,请保证所有实体的出现都是为了产生这个“改变”。
例如: i = i+1;
3)如果不能遵守1),那么请保证改变的是不同的实体。
例如:c = *p++;
4)如果1)和2)都不能遵守,那么请使用序列点将表达式分开。
例如 : (c = getchar()) != EOF && c != ‘\n’;
3. 优先级、结合方向做了哪些事,没有做哪些事?
3.1 优先级、结合方向做了哪些事?
C语言中组成程序的基本单位是表达式(expression),表达式是指用操作符(operator)和操作数(operand)连接起来的式子。C标准给出了最基本的操作符,通过这些操作符可以组成简单表达式,同样也可以通过复合产生复杂表达式。当一个表达式中出现多个操作符,多个操作数的时候,操作符合操作数是如何组合起来的呢?优先级和结合方向就是用来解决这个问题的,可以这么说,优先级和结合方向给出了一个表达式的含义,这只是说明了各个操作符和操作数是怎么聚合起来的。
3.2 优先级、结合方向没有做哪些事?
仅仅依靠优先级和结合方向是无法确定一个复合表达式中对各个子表达式的求值顺序。标准中对于这点的规定是:
两个相邻的操作符的执行顺序由它们的优先级决定。如果它们优先级相同,它们的执行顺序由它们的结合性决定。除此之外,编译器可以自由决定任何顺序对表达式进行求值,只要它不违反逗号,&&,||和?:操作符所施加的限制。
4. 解决问题
1)j = i++ + i++ + i++;
这个表达式违反了SS1,不同的编译器产生的结果可能不同。
2)arry[i] = i++;
这个表达式违反了SS2,不同的编译器产生的结果可能不同。
3)x = y = z = 1;
++x || ++y && ++z; PRINT(x, y, z);
&&的优先级比|| 的优先级高,所以:
++x || ++y && ++z 等效于++x || (++y && ++z)
这里就很容易犯错,会认为先执行++y && ++z 在执行++x || ( … ),这种观点是错误的,c中只对 逗号,、逻辑与 &&、
逻辑或 || 和 条件表达式规定了执行顺序,对于逻辑表达式方向是从左向右执行的。本例子中,先执行 ++x = 2,逻辑
或表达式被短路,++y && ++z没有执行,最后x = 2, y = 1, z = 1;
5.后记
通过本课题的学习加深了对于c的理解,理解SS1,SS2原则。在实际应用中应该遵守“一条语句只做一件事的原则”。