序列点和运算符优先级之间有什么区别?

时间:2017-06-26 23:34:51

标签: c++ c language-lawyer

考虑经典序列点示例:

i = i++;

C和C ++标准声明上述表达式的行为未定义,因为=运算符与序列点无关。

让我感到困惑的是,++的优先级高于=,因此,基于优先级的上述表达式必须先评估i++,然后再进行分配。因此,如果我们从i = 0开始,我们应该始终使用i = 0(或i = 1,如果表达式为i = ++i)而不是未定义的行为。我错过了什么?

3 个答案:

答案 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中,*的优先级高于+的优先级,因此bc将绑定到* }和expression将被解析为a + (b * c)

评估规则的顺序:它描述了如何在表达式中计算操作数。在声明中

 d = a>5 ? a : ++a; 
保证在评估a++b之前评估

c 但是对于表达式a + (b * c),尽管*的优先级高于+,但不能保证ab之前或之后进行评估或c甚至bc订购了他们的评估。即使abc也可以按任意顺序进行评估。

简单的规则是:运算符优先级独立于评估顺序,反之亦然。

在表达式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  

导致两个不同的结果01,这取决于通过赋值和++的副作用的顺序,因此调用UB。