【C语言】副作用、序列点和完整表达式与递增递减运算符的关系

副作用

副作用 ( 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圆括号中的表达式也叫做控制表达式,它们都是完整表达式,所以每个表达式的副作用(如,递增变量)都发生在对下一个表达式求值之前。

猜你喜欢

转载自blog.csdn.net/u014532291/article/details/107846687