序列点和运算符优先级有什么区别?
考虑经典的顺序点示例:
i = i++;
C和C ++标准声明上述表达式的行为是未定义的,因为=操作符不与序列点关联。
令我困惑的是, ++
的优先级高于=
,因此,基于优先级的上述表达式必须先评估i++
,然后再进行分配。 因此,如果我们从i = 0
开始,我们应该总是以i = 0
(或者i = 1
,如果表达式为i = ++i
)结束而不是未定义的行为。 我错过了什么?
运算符优先级(和关联性)说明表达式被解析和执行的顺序。 然而,这并没有说明操作数的评估顺序,这是一个不同的术语。 例:
a() + b() * c()
运算符优先级规定b()
的结果和c()
的结果必须相乘,然后再与a()
的结果相加。
但是,它没有提到应该执行这些功能的顺序。 每个操作员的评估顺序都指定了这一点。 大多数情况下,评估的顺序是未指定的(未指定的行为),这意味着标准可以让编译器以任何喜欢的顺序执行。 编译器不需要记录该顺序,也不需要保持一致。 这样做的目的是为编译器在表达式解析中提供更多的自由,这意味着编译速度更快,代码更快。
在上面的例子中,我编写了一个简单的测试程序,我的编译器以a()
, b()
, c()
的顺序执行了上述函数。 程序在可以乘以结果之前需要执行b()
和c()
事实并不意味着它必须以任何给定的顺序来评估这些操作数。
这是序列点的出现位置。它是程序中的一个给定点,必须完成所有先前的评估(和操作)。 所以序列点主要与评估顺序有关,而不是运算符优先级。
在上面的例子中,三个操作数相互之间没有序列关系,这意味着没有序列点决定了评估的顺序。
因此,当在这样的无序表达式中引入副作用时,就会出现问题。 如果我们编写i++ + i++ * i++
,那么我们仍然不知道这些操作数的评估顺序,所以我们无法确定结果会是什么。 这是因为+
和*
都具有未指定/无序的评估顺序。
如果我们写了i++ || i++ && i++
i++ || i++ && i++
,那么行为就会被很好地定义,因为&&
和||
指定评估的顺序从左到右,并且在评估左侧和右侧操作数之间有一个顺序点。 因此, if(i++ || i++ && i++)
是完全可移植且安全(尽管不可读)的代码。
至于表达式i = i++;
,这里的问题是=
被定义为(6.5.16):
更新左操作数的存储值的副作用在左和右操作数的值计算之后被排序。 操作数的评估是不确定的。
这个表达式实际上接近于明确定义,因为文本实际上表示在计算右操作数之前不应更新左操作数。 问题是最后一句话:操作数的评估顺序是不确定/不确定的。
由于表达式包含i++
的副作用,因此它会调用未定义的行为,因为我们无法知道操作数i
或操作数i++
是否先被计算。
(还有更多,因为该标准还指出,操作数不应该在表达式中用于不相关的目的两次,但这是另一回事。)
所有操作员都会产生结果。 另外,一些运算符(如赋值运算符=
和复合赋值运算符( +=
, ++
, >>=
等)会产生副作用。 结果和副作用之间的区别是这个问题的核心。
运算符优先级管理运算符应用于产生结果的顺序。 例如,优先级规则要求*
去之前+
, +
去之前&
,等等。
然而,运营商的优先权并未涉及应用副作用。 这是序列点(之前测序,测序之后等)进场的地方。 他们说,为了使表达明确,对存储器中相同位置的副作用的应用必须由序列点分开。
这个规则被i = i++
打破了,因为++
和=
将它们的副作用应用于同一个变量i
。 首先, ++
会更好,因为它具有更高的优先级。 它通过在增量之前取i
的原始值来计算它的值。 然后=
去,因为它具有较低的优先级。 其结果也是i
的原始价值。
这里缺少的关键是一个序列分隔两个操作员的副作用。 这就是行为未定义的原因。
运营商的优先顺序和评估顺序是两回事。 让我们逐个看看他们:
运算符优先规则:在表达式操作数中,优先级更高的运算符更紧密。
例如
int a = 5;
int b = 10;
int c = 2;
int d;
d = a + b * c;
在表达式a + b * c
,的优先级*
比的更高+
,因此, b
和c
将结合*
和表达式将被解析为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。
上一篇: What is the difference between a sequence point and operator precedence?