运算符优先级与求值顺序

术语“运算符优先级”和“评估顺序”是编程中非常常用的术语,并且对于程序员而言非常重要。 据我所知,这两个概念是紧密相连的。 在谈论表达式时,一个不能没有另一个。

让我们举一个简单的例子:

int a=1;  // Line 1
a = a++ + ++a;  // Line 2
printf("%d",a);  // Line 3

现在,很明显,x<(y<z)会导致未定义的行为,因为C和C ++中的序列点包括:

  1. 在对&&(逻辑 AND),|| (逻辑或)和逗号 操作员。 例如,在 表达式x<(y<z),全部 子表达式的副作用 (x<y)<z已完成,然后再尝试访问result

  2. 之间的三元数的第一个操作数的求值 “问号”运算符和 第二或第三操作数。 例如, 在表达式x<(y<z)中有一个序列点 第一个(x<y)<z,意味着它已经 被增加的时间 第二个实例被执行。

  3. 在完整表达的末尾。 此类别包括表达 陈述(例如作业 x<(y<z)),返回语句, 控制if,switch, while或do-while语句,以及所有 for语句中的三个表达式。

  4. 在函数调用中输入函数之前。顺序 不评估参数 指定,但此顺序点 意味着他们所有的副作用 在功能完成之前完成 输入。在表达式x<(y<z)中, (x<y)<z用 原始值result的参数, 但是i会在输入前增加 f的主体。类似地,jk是 在输入gh之前已更新 分别。但是,不是 按顺序指定f()g()h() ijk递增。值j和 因此f正文中的k undefined.3注意一个函数 呼叫f(a,b,c)不是使用 逗号运算符和的顺序 abc的评估为 未指定。

  5. 在函数返回时,将返回值复制到 调用上下文。 (此顺序点 仅在C ++标准中指定; 它仅隐式存在于 C。)

  6. 在初始化程序的末尾; 例如,评估5 在声明x<(y<z)中。

因此,按照第3点进行:

在完整表达的末尾。 此类别包括表达式语句(例如赋值a = b;),返回语句,if,switch,while或do-while语句的控制表达式以及for语句中的所有三个表达式。

x<(y<z)显然会导致未定义的行为。 这显示了未定义行为如何与序列点紧密结合。

现在让我们再举一个例子:

int x=10,y=1,z=2; // Line 4
int result = x<y<z; // Line 5

现在很明显x<(y<z)将使变量(x<y)<z存储result

现在,可以将(x<y)<z中的表达式x<(y<z)评估为:

x<(y<z)或29920646282530003000705。在第一种情况下,2992064628258253000706的值将是2992064628269777977920,在第二种情况下,2992064628228269777921的值将是printf()。但是我们知道,当299206462828269777923是x时-2992064628269777777起作用了,因此,是29646646269269777起作用了,因此,

这是此MSDN文章中所说的:

C运算符的优先级和关联性会影响表达式中操作数的分组和评估。 仅当存在其他具有较高或较低优先级的运算符时,运算符的优先级才有意义。 首先评估具有更高优先级运算符的表达式。 优先级也可以用“绑定”一词来描述。 优先级较高的运算符具有更严格的绑定。

现在,关于以上文章:

它提到“先评估具有较高优先级的运算符的表达式”。

听起来可能不正确。 但是,如果我们认为2992064664628269777920也是运算符left-to-rightprintf()相同,则我认为这篇文章并没有说错。我的理由是,如果不发挥关联性,则完整的表达式求值将变得模棱两可,因为2992064628269777777923不是 序列点。

另外,我发现的另一个链接在操作符优先级和关联性上说了这一点:

本页按优先顺序(从高到低)列出C运算符。 它们的关联性指示在表达式中应用相同优先级的运算符的顺序。

因此,以2992064628269769777920的第二个示例为例,我们可以在这里看到所有3个表达式都有2992064628228269777921、printf()myval,因为表达式的最简单形式由单个文字常量或对象组成。 因此,表达式xyz的结果将是右值,即分别为1012。 因此,现在我们可以将x<y<z解释为10<1<2

现在,关联性没有发挥作用,因为现在我们有2个要评估的表达式,即2992064628228269777920或2992064628269769777921,并且由于运算符的优先级相同,因此它们从左到右进行评估?

以最后一个例子作为我的论点:

int myval = ( printf("Operator\n"), printf("Precedence\n"), printf("vs\n"),
printf("Order of Evaluation\n") );

现在在上面的示例中,由于2992064628269777977920运算符具有相同的优先级,因此对表达式进行评估2992064628269777977921并将最后一个printf()的返回值存储在myval中。

在SO / IEC 9899:201x中J.1未指定的行为中,它提到:

评估子表达式的顺序和副作用的顺序发生,但函数调用(),&&,||,?:和逗号指定的除外运算符(6.5)。

现在我想知道,说错了吗?

评估顺序取决于运算符的优先级,从而留下未指定行为的情况。

如果我在问题中说的话有任何错误,我想纠正。我发布此问题的原因是由于MSDN文章在我的脑海中造成了混乱。 是否有错误?

Sadique asked 2020-08-01T10:46:38Z
6个解决方案
43 votes

是的,至少在标准C和C ++ 1方面,MSDN文章有误。

话虽如此,让我从术语的注释开始:在C ++标准中,它们(主要是-有一些误用)使用“求值”来指代操作数,而使用“值计算”来指代。 进行手术。 因此,例如,当您执行2992067218235735720704时,将对2992067218235735720705和2992067218235735720706中的每一个进行评估,然后进行值计算以确定结果。

显然,值计算的顺序(主要)是由优先级和关联性控制的-控制值计算基本上是什么是优先级和关联性的定义。 该答案的其余部分使用“评估”来指代操作数的评估,而不是指计算值。

现在,对于由优先级确定的评估顺序,不,不是! 就这么简单。 例如,让我们考虑您的示例2992067218235735720704。根据关联性规则,此解析为2992067218235735720705。现在,考虑在堆栈机上评估此表达式。 完全可以这样做:

 push(z);    // Evaluates its argument and pushes value on stack
 push(y);
 push(x);
 test_less();  // compares TOS to TOS(1), pushes result on stack
 test_less();

它在2992067218218235720705或2992067218235735720706之前先评估2992067218235735720704,但仍然评估2992067218235735720707,然后将比较结果与z进行比较,如预期的那样。

摘要:评估顺序与关联性无关。

优先顺序是相同的。 我们可以将表达式更改为2992067218218235720704,并在2992067218235735720706或c之前仍然评估a

push(z);
push(y);
push(x);
mul();
add();

摘要:评估顺序与优先顺序无关。

当/如果我们增加副作用,则保持不变。 我认为将副作用视为由单独的执行线程来执行是有教育意义的,在下一个序列点(例如表达式的末尾)带有2992067218218235720704。 所以像a这样的东西可以像这样执行:

push(a);
push(b);
push(c+1);
side_effects_thread.queue(inc, b);
side_effects_thread.queue(inc, c);
add();
assign();
join(side_effects_thread);

这也说明了为什么表面上的依存关系也不一定会影响评估顺序的原因。 即使2992067218235735720704是分配的目标,它仍然会在评估b或299206721823572020707之前先评估a。还要注意,尽管我在上面将其写为“线程”,但它也可能是线程池,所有线程都在执行 同时进行,因此您无法保证一个增量与另一个增量的顺序。

除非硬件直接(廉价)支持线程安全排队,否则可能不会在实际实现中使用它(即使那样也不太可能)。 通常,将某些内容放入线程安全队列中要比执行单个增量多得多的开销,因此很难想象有人在现实中这样做。 但是,从概念上讲,该想法符合标准的要求:当您使用前/后递增/递减运算时,您要指定一个运算,该运算将在表达式的该部分被求值后的某个时间发生,并在 下一个序列点。

编辑:尽管不是完全线程化,但某些体系结构确实允许这种并行执行。 举几个例子,Intel Itanium和VLIW处理器(例如某些DSP)允许编译器指定要并行执行的许多指令。 大多数VLIW机器都有特定的指令“数据包”大小,该大小限制了并行执行的指令数量。 Itanium还使用指令包,但在指令包中指定一位以表示当前包中的指令可以与下一个包中的指令并行执行。 使用这样的机制,您可以并行执行指令,就像您在我们大多数人都熟悉的架构上使用了多个线程一样。

摘要:评估顺序与明显的依赖关系无关

在下一个序列点之前使用该值的任何尝试都会产生不确定的行为-特别是,“其他线程”正在(潜在地)在那段时间内修改该数据,并且您无法与其他线程同步访问。 任何使用它的尝试都会导致不确定的行为。

仅举一个(公认的,现在牵强的例子),请考虑您的代码在64位虚拟机上运行,但是真正的硬件是8位处理器。 当您递增64位变量时,它将执行类似以下的序列:

load variable[0]
increment
store variable[0]
for (int i=1; i<8; i++) {
    load variable[i]
    add_with_carry 0
    store variable[i]
}

如果您在该序列中间的某个位置读取该值,则可能只修改了一些字节就可以得到某些内容,因此您得到的既不是旧值也不是新值。

这个确切的示例可能牵强附会,但是不太极端的版本(例如32位计算机上的64位变量)实际上是相当普遍的。

结论

评估顺序不依赖于优先级,关联性或(必要时)依赖于表观依赖性。 尝试使用在表达式的任何其他部分中应用了前/后递增/递减的变量确实会产生完全未定义的行为。 虽然不太可能发生实际的崩溃,但是绝对不能保证您会获得旧值或新值-您可能会完全获得其他值。


1我还没有看过这篇特别的文章,但是很多MSDN文章都谈到了Microsoft的Managed C ++和/或C ++ / CLI(或特定于C ++的实现),但是几乎没有做任何事情来指出他们没有 适用于标准C或C ++。 这可能会带来错误的外观,即他们声称自己决定将适用于自己的语言的规则实际上适用于标准语言。 在这些情况下,这些文章在技术上并不是错误的-它们与标准C或C ++毫无关系。 如果您尝试将这些语句应用于标准C或C ++,则结果为false。

Jerry Coffin answered 2020-08-01T10:48:20Z
12 votes

优先顺序影响评估顺序的唯一方法是创建依赖关系; 否则两者是正交的。 你已经精心选择的琐碎示例,其中依赖项由优先权最终会完全定义评估顺序,但这不是通常是正确的。 而且也不要忘记,很多表达式都有两种效果:它们产生价值,并且具有副作用。 这些不需要两个一起出现,所以即使依赖强制执行特定的评估顺序,这只是顺序价值评估; 它对副作用没有影响。

James Kanze answered 2020-08-01T10:48:43Z
7 votes

解决这个问题的一种好方法是采用表达式树。

如果您有表达式,可以说x + ( y * z ) , z = 10, x + ( y * z),您可以将其重写为表达式树:

应用优先级和关联性规则:

x + ( y * z )

应用优先级和关联性规则后,您可以放心地忘记它们。

树状形式:

  x
+
    y
  *
    z

现在该表达式的叶子是x + ( y * z ) , z = 10, x + ( y * z)y和2992069477254254300674。这意味着您可以按任意顺序评估xyz,也意味着您可以按任意顺序评估2992069477254300300和x的结果。

现在,由于这些表达式没有副作用,因此您不必担心。 但是如果这样做的话,排序可以改变结果,并且由于排序可以是编译器决定的任何事情,因此您就遇到了问题。

现在,序列点使这个混乱变得有些混乱。 他们有效地将树切成段。

x + ( y * z ) , z = 10, x + ( y * z)

优先权和关联性之后

x + ( y * z ) , z = 10, x + ( y * z)

那个树:

      x
    +
        y
      *
        z
  , ------------
      z
    =
      10     
  , ------------
      x
    +
        y
      *
        z   

树的顶部将在中间位置之前进行评估,在底部之前进行评估。

Let_Me_Be answered 2020-08-01T10:49:48Z
4 votes

它提到“先评估具有较高优先级的运算符的表达式”。

我只想重复我在这里说的话。 就标准C和C ++而言,本文存在缺陷。 优先级仅会影响将哪些标记视为每个运算符的操作数,但不会以任何方式影响评估顺序。

因此,该链接仅说明Microsoft如何实现事情,而不说明语言本身的工作方式。

Prasoon Saurav answered 2020-08-01T10:50:17Z
2 votes

优先顺序与评估顺序无关,反之亦然。

优先规则描述了当表达式混合使用不同类型的运算符时,应在括号内的表达式中加上括号的情况。 例如,乘法的优先级高于加法,因此0等效于--y,而不是(++x) || (--y)

评估规则的顺序描述了对表达式中每个操作数进行评估的顺序。

举个例子

y = ++x || --y;   

根据运算符优先级规则,将其括弧为(0的优先级高于--y的优先级,而--y的优先级高于(++x) || (--y)):

y = ( (++x) || (--y) )   

逻辑OR的评估顺序0指出(C11 6.5.14)

|| 操作员保证从左到右的评估。

这意味着将首先评估左操作数,即子表达式0。 由于短路行为; 如果第一个操作数的值不等于0,则不评估第二个操作数,尽管右操作数--y的括号前为(++x) || (--y),但不会评估右操作数。

haccks answered 2020-08-01T10:51:09Z
-1 votes

我认为这只是

a++ + ++a

表达有问题,因为

a = a++ + ++a;

首先适合3.,然后适合6.规则:分配前完成评估。

所以,

a++ + ++a

得到1的完全评估值:

1 + 3   // left to right, or
2 + 2   // right to left

结果是相同的= 4。

一个

a++ * ++a    // or
a++ == ++a

会有不确定的结果。 是不是

mb84 answered 2020-08-01T10:52:00Z
translate from https://stackoverflow.com:/questions/5473107/operator-precedence-vs-order-of-evaluation