考虑经典序列点示例:
i = i++;
C和C ++标准声明上述表达式的行为未定义,因为=运算符与序列点无关。
让我感到困惑的是,++
的优先级高于=
,因此,基于优先级的上述表达式必须先评估i++
,然后再进行分配。因此,如果我们从i = 0
开始,我们应该始终使用i = 0
(或i = 1
,如果表达式为i = ++i
)而不是未定义的行为。我错过了什么?
答案 0 :(得分:30)
所有操作员都会生成结果。此外,一些运算符(例如赋值运算符=
和复合赋值运算符(+=
,++
,>>=
等)会产生副作用。结果和副作用之间的区别是这个问题的核心。
运算符优先级控制运算符应用于生成结果的顺序。例如,优先规则要求*
在+
之前,+
在&
之前,依此类推。
但是,运算符优先级没有说明应用副作用。这是序列点(之前测序,后测序等)发挥作用的地方。他们说,为了使表达式得到明确定义,副作用在内存中相同位置的应用必须用序列点分隔。
i = i++
打破了此规则,因为++
和=
都将其副作用应用于同一变量i
。首先,++
,因为它具有更高的优先级。它通过在增量之前取i
的原始值来计算其值。然后是=
,因为它的优先级较低。其结果也是i
的原始值。
这里缺少的关键是分离两个运算符的副作用的序列点。这就是行为未定义的原因。
答案 1 :(得分:13)
运算符优先级(和关联性)表示解析和执行表达式的顺序。但是,这并没有说明操作数的评估顺序,这是一个不同的术语。例如:
a() + b() * c()
运算符优先级指示b()
的结果和c()
的结果必须相乘才能与a()
的结果一起添加。
但是,它没有说明应该执行这些功能的顺序。每个运算符的评估顺序指定了这一点。大多数情况下,评估的顺序是未指定的(未指定的行为),这意味着标准允许编译器以它喜欢的任何顺序执行它。编译器不需要记录此顺序,也不需要一致地执行操作。这样做的原因是为编译器提供了更多的表达式解析自由度,这意味着更快的编译速度以及可能更快的代码。
在上面的例子中,我编写了一个简单的测试程序,我的编译器按照a()
,b()
,c()
的顺序执行了上述函数。程序需要在它可以乘以结果之前执行b()
和c()
这一事实并不意味着它必须以任何给定的顺序评估这些操作数。
这是序列点的来源。它是程序中的一个给定点,必须完成所有先前的评估(和操作)。因此,序列点主要与评估顺序相关,而不是运算符优先级。
在上面的例子中,三个操作数相互之间是未序列,这意味着没有序列点决定评估的顺序。
因此,当在这种未经测序的表达中引入副作用时,它会变成问题。如果我们写i++ + i++ * i++
,那么我们仍然不知道这些操作数的计算顺序,因此我们无法确定结果是什么。这是因为+
和*
都有未指定/未按顺序排列的评估。
如果我们写了i++ || i++ && i++
,那么行为就会明确定义,因为&&
和||
指定评估的顺序是从左到右,并且有一个左右操作数的评估之间的序列点。因此if(i++ || i++ && i++)
是完全可移植且安全(尽管不可读)的代码。
至于表达式i = i++;
,这里的问题是=
被定义为(6.5.16):
在左右操作数的值计算之后,对更新左操作数的存储值的副作用进行排序。对操作数的评估是不合理的。
这个表达式实际上接近定义明确,因为文本实际上表示在计算右操作数之前不应更新左操作数。问题是最后一句:操作数的评估顺序未指定/未排序。
由于表达式包含i++
的副作用,因此它会调用未定义的行为,因为我们无法知道操作数i
或操作数i++
是否先被计算。
(还有更多内容,因为标准还说操作数不应该在表达式中用于不相关的目的,但这是另一个故事。)
答案 2 :(得分:0)
运营商优先级和评估顺序是两回事。让我们一个一个地看看它们:
运算符优先级规则:在表达式中,操作数绑定到具有更高优先级的运算符。
例如
int a = 5;
int b = 10;
int c = 2;
int d;
d = a + b * c;
在表达式a + b * c
中,*
的优先级高于+
的优先级,因此b
和c
将绑定到*
}和expression将被解析为a + (b * c)
。
评估规则的顺序:它描述了如何在表达式中计算操作数。在声明中
d = a>5 ? a : ++a;
保证在评估a
或++b
之前评估 c
但是对于表达式a + (b * c)
,尽管*
的优先级高于+
,但不能保证a
在b
之前或之后进行评估或c
甚至b
和c
订购了他们的评估。即使a
,b
和c
也可以按任意顺序进行评估。
简单的规则是:运算符优先级独立于评估顺序,反之亦然。
在表达式i = i++
中,++
的更高优先级只是告诉编译器将i
与++
运算符绑定在一起。它没有说明操作数的评估顺序或者应该首先发生哪个副作用(=
运算符或++
运算符)。编译器可以自由地做任何事情。
让我们将作业左侧的i
重命名为il
,并在作业的右侧(在i++
)中重命名为ir
,然后表达式就像
il = ir++ // Note that suffix l and r are used for the sake of clarity.
// Both il and ir represents the same object.
现在编译器可以自由地将表达式il = ir++
评估为
temp = ir; // i = 0
ir = ir + 1; // i = 1 side effect by ++ before assignment
il = temp; // i = 0 result is 0
或
temp = ir; // i = 0
il = temp; // i = 0 side effect by assignment before ++
ir = ir + 1; // i = 1 result is 1
导致两个不同的结果0
和1
,这取决于通过赋值和++
的副作用的顺序,因此调用UB。