副作用
副作用 ( side effect ) 是对数据对象或文件的修改
副作用其实就是表达式的主要目的,例如:
num = 50;
这条表达式的副作用是将变量的值设置为50。虽然看起来像是主要目的,但是从C语言的角度看,主要目的是对表达式求值。
给出表达式 4 + 6,C会对其求值得10;给出表达式num = 50,C会对其求值得50。对该表达式求值的副作用是把变量num的值改为50。
跟赋值运算符一样,递增和递减运算符也有副作用,使用它们的主要目的就是使用其副作用。
类似地,调用printf()函数时显示的信息其实是副作用。
序列点
序列点 (sequence point) 是程序执行的点,该点之前的副作用都会在进入下一步之前发生
&&和||符号都是序列点,并且具备短路特性,某个元素让整个表达式无效后便立即停止求值
任何一个完整表达式的结束也是一个序列点
在C语言中,语句中的分号标记了一个序列点,所以分号前所有的副作用都会发生
例如:
for (int i = 0; i < 100; i++){
...
}
括号中的分号各代表了一个序列点,int i = 0 的副作用 —— 把0赋值给 i 需要在第一个分号之前完成;同样地,i < 100 的副作用判断需要在第二个分号前完成。
接下来再看一个例子:
while (guests++ < 10)
printf("%d\n", guests);
很多人会认为,这里的 guests++ 是“先使用它,再递增它”的意思。但是,该表达式 (guests++ < 10)是一个完整表达式(后面会说到),因为它是while循环的测试条件,所以该表达式的结束就是一个序列点。因此,C保证了程序在执行printf()函数之前发生副作用,即递增guests。同时,这里递增运算符的后缀形式保证了guests在完成与10的比较后才进行递增。
这里可能有一些疑问,为什么不能用 “先使用,后递增” 来解释后缀形式的递增运算符呢?
下面看一个特殊情况:
// 尽量避免编写这种语句
y = (4 + x++) + (6 + x++);
上面的 (4 + x++) 是一个子表达式,并不是一个完整表达式,所以C无法保证x在子表达式 4 + x++ 求值后递增x。这里,完整表达式是整个赋值表达式语句,分号标记了序列点。所以,C 保证程序在执行下一条语句之前递增x两次。C并未指明是在对子表达式求值以后递增x,还是对所有表达式求值后再递增x。
如果理解为“先使用,后递增”,那么应该如何定义“使用”呢,是先对两个表达式求值后再递增两次x,还是在第一个子表达式结束后就递增了x?不同编译器可能会照成不同的结果。因此, 要尽量避免编写类似的语句。
同样地:
n = 3;
y = n++ + n++; // y可能是6, 也可能是7
可以肯定的是,执行完这两条语句后,n的值会比旧值(3)大2。但是,y的值不确定。在对y求值时,编译器可以使用n的旧值(3)两次,然后把n递增1两次,这使得y的值为6,n的值为5。或者,编译器使用n的旧值(3)一 次,立即递增n,再对表达式中的第2个n使用递增后的新值,然后再递增n, 这使得 y 的值为 7,n 的值为 5。两种方案都可行。对于这种情况更精确地说,结果是未定义的,这意味着C标准并未定义结果应该是什么。
完整表达式
完整表达式(full expression)就是指这个表达式不是另一个更大表达式的子表达式
表达式语句中的表达式和while循环中的作为测试条件的表达式,都是完整表达式。 序列点有助于分析后缀递增何时发生。
for圆括号中的表达式也叫做控制表达式,它们都是完整表达式,所以每个表达式的副作用(如,递增变量)都发生在对下一个表达式求值之前。