运算符优先级与评估顺序

Sad*_*que 44 c c++ operator-precedence

术语"运算符优先级"和"评估顺序"是编程中非常常用的术语,对于程序员来说非常重要.而且,据我所知,这两个概念紧密相连; 在谈论表达时,一个人离不开另一个人.

我们举一个简单的例子:

int a=1;  // Line 1
a = a++ + ++a;  // Line 2
printf("%d",a);  // Line 3
Run Code Online (Sandbox Code Playgroud)

现在,很明显会Line 2导致未定义的行为,因为C和C++中的序列点包括:

  1. 在评估&&(逻辑AND)的左右操作数之间,|| (逻辑OR)和逗号运算符.例如,在表达式中*p++ != 0 && *q++ != 0,子表达式的所有副作用 *p++ != 0都在任何访问尝试之前完成q.

  2. 在评估三元"问号"运算符的第一个操作数和第二个或第三个操作数之间.例如,在表达式中,在第a = (*p++) ? (*p++) : 0一个之后存在一个序列点*p++,这意味着它已经在第二个实例执行时增加了.

  3. 在完整表达结束时.此类别包括表达式语句(例如赋值 a=b;),return语句,if,switch,while或do-while语句的控制表达式,以及for语句中的所有三个表达式.

  4. 在函数调用中输入函数之前.未指定参数的计算顺序,但此序列点表示在输入函数之前所有副作用都已完成.在表达式中f(i++) + g(j++) + h(k++), f使用原始值的参数调用i,但i在进入正文之前递增f.同样,jk正在进入前更新gh 分别.然而,不指定以何种顺序f(),g(),h() 被执行时,也不能以何种顺序i,j, k递增.因此,身体的jk在身体中的值f是不确定的.3注意函数调用f(a,b,c)不是用逗号和评价的顺序a,bc是不确定的.

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

  6. 在初始化器结束时; 例如,在声明中评估5之后int a = 5;.

因此,按点#3:

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

Line 2显然会导致未定义的行为.这显示了Undefined Behavior如何与Sequence Points紧密结合.

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

int x=10,y=1,z=2; // Line 4
int result = x<y<z; // Line 5
Run Code Online (Sandbox Code Playgroud)

现在很明显,这Line 5将使变量result存储1.

现在表达x<y<zLine 5可以评价为任一:

x<(y<z)(x<y)<z.在第一种情况下,值result将是,0而在第二种情况下result将是1.但我们知道,当Operator PrecedenceIS Equal/Same- Associativity进场,因此,作为评价(x<y)<z.

这是MSDN文章中的内容:

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

现在,关于以上文章:

它提到"首先评估具有高优先级运算符的表达式".

听起来可能不正确.但是,如果我们认为这()也是一个与运营商x<y<z相同的话,我认为这篇文章并没有说错(x<y)<z.我的理由是,如果关联性没有发挥作用,那么完整的表达式评估将变得模棱两可,因为<它不是一个序列点.

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

此页面按优先顺序(从最高到最低)列出C运算符.它们的关联性表示应用表达式中具有相同优先级的运算符的顺序.

因此,在第二个例子中int result=x<y<z,我们可以看到这里有3个表达式x,y并且z,因为,表达式的最简单形式由单个文字常量或对象组成.因此,表达式的结果x,y,z将在那里右值,即10,12分别.因此,现在我们可以解释x<y<z10<1<2.

现在,没有关联性发挥作用,因为现在我们有2个表达式进行评估,无论是10<11<2并且由于运营商的优先级是一样的,他们是评估自左向右

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

int myval = ( printf("Operator\n"), printf("Precedence\n"), printf("vs\n"),
printf("Order of Evaluation\n") );
Run Code Online (Sandbox Code Playgroud)

现在在上面的例子中,由于comma运算符具有相同的优先级,因此将计算表达式left-to-rightprintf()存储last的返回值myval.

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

除了为函数调用(),&&,||,?:和逗号运算符(6.5)指定的情况外,评估子表达式的顺序和副作用的顺序除外.

现在我想知道,说:是不对的:

评估顺序取决于运营商的优先级,留下未指定行为的情况.

如果我在问题中说的话有任何错误,我想纠正.我发布此问题的原因是因为MSDN文章在我的脑海中产生了混淆.它是否出错

Jer*_*fin 43

是的,MSDN文章是错误的,至少就标准C和C++ 1而言.

话虽如此,让我先谈一下关于术语的说明:在C++标准中,他们(主要是 - 有一些漏洞)使用"评估"来指代评估操作数,而"值计算"指的是进行手术.因此,当(例如)你做a + b,每个ab被评估时,然后执行值计算以确定结果.

很明显,值计算的顺序是(主要)由优先级和结合控制-控制值计算基本上就是优先级和结合的定义.本答案的其余部分使用"评估"来指代操作数的评估,而不是值计算.

现在,至于评估顺序是由优先级决定的,不是不是!就这么简单.例如,让我们考虑你的例子x<y<z.根据关联性规则,这解析为(x<y)<z.现在,考虑在堆栈计算机上评估此表达式.完全允许它做这样的事情:

 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();
Run Code Online (Sandbox Code Playgroud)

这会zx或之前进行评估y,但仍会进行评估(x<y),然后将该比较的结果z与其预期进行比较.

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

优先权是一样的.我们可以改变的表达x*y+z,而且还评估z之前x或者y:

push(z);
push(y);
push(x);
mul();
add();
Run Code Online (Sandbox Code Playgroud)

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

当/如果我们添加副作用,这仍然是相同的.我认为将副作用视为由单独的执行线程执行,并join在下一个序列点(例如,表达式的结尾)进行处理是有教育意义的.所以像这样的东西a=b++ + ++c;可以执行:

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);
Run Code Online (Sandbox Code Playgroud)

这也说明了为什么明显的依赖性也不一定影响评估的顺序.即使a是作业的目标,这仍然在评估a 之前评估bc.还要注意,尽管我上面写的是"线程",但这也可能是一个线程,所有线程都并行执行,所以你不能保证一个增量的顺序与另一个增量的顺序.

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

编辑:虽然它不是完全线程,但是一些架构确实允许这样的并行执行.举几个例子,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]
}
Run Code Online (Sandbox Code Playgroud)

如果你在该序列的中间某处读取了值,那么你可以得到只修改了一些字节的东西,所以你得到的既不是旧值也不是新值.

这个确切的例子可能相当牵强,但是一个不太极端的版本(例如,32位机器上的64位变量)实际上相当普遍.

结论

评估顺序依赖于优先级,关联性或(必然)依赖于明显的依赖性.尝试使用在表达式的任何其他部分中应用了前/后递增/递减的变量确实给出了完全未定义的行为.虽然实际崩溃的可能性不大,但你绝对不能保证获得旧值或新值 - 你可以完全得到其他东西.


1我没有查看过这篇特别的文章,但是很多MSDN文章都谈到了微软的托管C++和/或C++/CLI(或者特定于他们的C++实现),但很少或根本没有指出他们没有适用于标准C或C++.这可能会给出错误的外观,即他们声称他们决定适用于他们自己的语言的规则实际上适用于标准语言.在这些情况下,文章在技术上并不是错误的 - 它们与标准C或C++没有任何关系.如果尝试将这些语句应用于标准C或C++,则结果为false.

  • 由于优先级和关联性规则定义了表达式树的形状,因此它们还定义了部分排序,因为必须从树叶到根评估树.叶子本身之间当然没有排序,但声称根本没有任何排序只是奇怪. (2认同)

Jam*_*nze 12

优先级影响评估顺序的唯一方法是创建依赖关系; 否则两者是正交的.您仔细选择了一些简单的示例,其中由优先级创建的依赖关系最终会完全定义评估顺序,但这通常不正确.并且不要忘记,许多表达式有两个效果:它们会产生一个值,并且它们会产生副作用.这两者不需要一起发生,因此即使依赖强制执行特定的评估顺序,这只是值的评估顺序; 它对副作用没有影响.


Let*_*_Be 7

看一下这个的好方法是使用表达式树.

如果你有一个表达式,假设x+y*z你可以将它重写为表达式树:

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

x + ( y * z )
Run Code Online (Sandbox Code Playgroud)

应用优先级和关联性规则后,您可以安全地忘记它们.

以树形式:

  x
+
    y
  *
    z
Run Code Online (Sandbox Code Playgroud)

现在这个表达式的叶子是x,yz.这意味着,你可以评估x,yz在你想要的,和任何顺序也意味着你可以评估的结果*,并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   
Run Code Online (Sandbox Code Playgroud)

树的顶部将在中间之前评估,中间在底部之前评估.


Pra*_*rav 5

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

我只是想重复一下我在这里所说的话。就标准 C 和 C++ 而言,这篇文章是有缺陷的。优先级仅影响哪些标记被视为每个运算符的操作数,但它不会以任何方式影响计算顺序。

因此,该链接仅解释了 Microsoft 如何实现事物,而不解释该语言本身如何工作。


hac*_*cks 5

优先级与求值顺序无关,反之亦然。

优先级规则描述了当表达式混合不同类型的运算符时,应如何为带下括号的表达式添加括号。例如,乘法的优先级高于加法,因此2 + 3 x 4相当于2 + (3 x 4), not(2 + 3) x 4

求值规则的顺序描述了表达式中每个操作数的求值顺序。

举个例子

y = ++x || --y;   
Run Code Online (Sandbox Code Playgroud)

根据运算符优先级规则,它将被括号括起来,因为 (++/--的优先级高于||, 的优先级高于=):

y = ( (++x) || (--y) )   
Run Code Online (Sandbox Code Playgroud)

逻辑 OR 的求值顺序||指出 (C11 6.5.14)

|| 的 运算符保证从左到右的评估。

(x++)这意味着将首先计算左操作数,即子表达式。由于短路行为;如果第一个操作数与 比较不等于0,则不计算第二个操作数--y即使右操作数在括号之前,也不会计算(++x) || (--y)