3.10.3 未定义的行为
值得注意的是,逻辑或表达式的求值具有短路效应。这是什么意思呢?逻辑或表达式求值时,是先求值运算符||的左操作数,如果左操作数的值不为0,则不再求值右操作数,因为这样做是多余的。只有在左操作数的值为0时,才会继续求值右操作数。
非但如此,C语言还规定,如果运算符||的右操作数会被求值(这意味着左操作数求值的结果为0),则在其左操作数的求值和右操作数的求值之间存在一个序列点。换句话说,在求值右操作数之前,左操作数的值计算和副作用已经全部完成。
来看一个例子,如果标识符n被声明为一个整数类型的变量,则表达式n ++ || n求值时,只有在表达式n ++的值计算和副作用已经完成,且该表达式的值为0时,才开始求值表达式n。
如果在表达式n ++ || n求值前,变量n的当前值是0,而且我们把表达式n的值计算记为Vn,把表达式n ++的值计算记为V++,副作用记为S++,整个逻辑或表达式的值(逻辑或运算符的结果)记为V||,则该表达式的求值过程如下:
V++(0)→ S++(1)→ Vn(1)→ V||(1)
这里,是先求值左操作数n ++,且值计算和副作用都已经完成(已经把1作为新值写入变量n),当右操作数求值时,左值转换的结果是刚刚写入的1。因为这两个操作数求值后的值一个为0、一个为1,整个逻辑或表达式的值为1。
注意,因为序列点的存在,上述求值过程是唯一的,不存在其他可能性。如果没有上述序列点的保证,则这两个表达式的求值将有可能交错地进行,其最终结果无法预料。比如说它可能是这样的:
Vn(0)→ V++(0)→ V||(0)→ S++(1)
显然,因为逻辑运算符||的左操作数和右操作数在求值后都是0,所以整个逻辑或表达式的值也为0。
作为一门编程语言,C语言的规范描述了程序结构、语法元素、表达式、语句,定义了它们的形式和操作,规定了操作数的类型和范围,同时也描述了可预期的运行结果。但是对于不遵循规范的程序设计,C语言没有,也无法限定和描述程序的运行结果。在这种情况下,程序的行为是无法预料的,计算结果可能碰巧是正确的,也可能是错的,程序可能会崩溃,等等,不一而足,这些无法预料的行为,称为未定义的行为。
来看另一个例子,在下面的代码片断中,表达式m = m ++的求值就是未定义的行为,求值完成后,变量m的存储值不能确定,取决于不同的翻译器如何安排求值过程。
int m = 0; m = m ++;
表达式m = m ++的求值具有两个副作用,分别是运算符++的副作用和运算符=的副作用,但都是修改变量m的存储值,这就很特殊了。这两个副作用哪个在前哪个在后,C语言并未规定,要由翻译器自主决定。这就是说,该表达式求值的行为是未定义的。因为这个原因,该表达式求值完成后,变量m的值可能是0,也可能是1。
在任何一个用C语言编写的程序中,很多行为是良好定义的,比如C语言规定在全表达式的求值之间有序列点,在运算符||的左操作数和右操作数的求值之间存在序列点。正是有了特殊规定,表达式n ++ || n的求值不存在未定义的行为。
练习3.9
1.我们已经讲过,赋值表达式的值是赋值运算符的左操作数被赋值之后的值,赋值运算符的副作用发生在赋值运算符左右操作数的值计算(而不是求值)之后。依据这一规定,请说明表达式n = n + 1和sum = sum + n不存在未定义的行为。
2.若y是整数类型的变量,判断表达式y =(y = 0)+ 3的求值是否存在未定义的行为。