27

介绍

在每本关于 C/C++ 的教科书中,您都会找到一个运算符优先级和关联性表,如下所示:

运算符优先级和关联性表

http://en.cppreference.com/w/cpp/language/operator_precedence

StackOverflow 上的一个问题是这样的:

以下函数执行的顺序是什么:

f1() * f2() + f3();
f1() + f2() * f3();

参考前面的图表,我自信地回答说函数具有从左到右的关联性,因此在前面的语句中,在两种情况下都像这样评估:

f1() -> f2() -> f3()

评估功能后,您可以像这样完成评估:

(a1 * a2) + a3
a1 + (a2 * a3)

令我惊讶的是,很多人告诉我我完全错了。决心证明他们错了,我决定转向 ANSI C11 标准。我再次惊讶地发现,很少有人提到运算符优先级和关联性。

问题

  1. 如果我认为函数总是从左到右进行评估的信念是错误的,那么表中提到函数优先级和关联性的真正含义是什么?
  2. 如果不是 ANSI,谁来定义运算符优先级和关联性?如果是ANSI做出定义,为什么很少提及运算符优先级和关联性?运算符优先级和关联性是从 ANSI C 标准推断出来的,还是在数学中定义的?
4

5 回答 5

43

运算符优先级在适当的标准中定义。C 和 C++ 的标准是 C 和 C++ 到底是什么的一个真正定义。所以如果你仔细观察,细节就在那里。事实上,细节在语言的语法中。例如,看一下C++中+和的语法产生规则(统称为additive-expressions):-

additive-expression:
  multiplicative-expression
  additive-expression + multiplicative-expression
  additive-expression - multiplicative-expression

如您所见,乘法表达式加法表达式的子规则。这意味着如果你有类似的东西x + y * z,则y * z表达式是 的子表达式x + y * z。这定义了这两个运算符之间的优先级。

我们还可以看到加法表达式的左操作数扩展为另一个加法表达式,这意味着 withx + y + zx + y它的子表达式。这定义了关联性

关联性决定了如何对同一运算符的相邻使用进行分组。例如,+是从左到右关联的,这意味着x + y + z将像这样分组:(x + y) + z.

不要将其误认为是评估顺序。绝对没有理由z无法计算之前的x + y值。重要的是它x + y是计算出来的,而不是计算出来的y + z

对于函数调用运算符,从左到右的关联性意味着f()()(例如,如果f返回函数指针,可能会发生这种情况)按如下方式分组:((f())()当然,另一个方向没有任何意义)。

现在让我们考虑您正在查看的示例:

f1() + f2() * f3()

运算符的*优先级高于+运算符,因此表达式按如下方式分组:

f1() + (f2() * f3())

我们甚至不必在这里考虑关联性,因为我们没有任何相同的运算符彼此相邻。

然而,函数调用表达式的求值是完全无序的。没有理由f3不能先调用,然后调用,然后f1调用f2。在这种情况下,唯一的要求是运算符的操作数在运算符之前进行评估。所以这意味着f2并且f3必须在评估之前调用*并且必须*评估并且f1必须在评估之前调用+

然而,一些运算符确实对其操作数的求值进行了排序。例如, in x || y,x总是在 之前计算y。这允许短路,如果已知是,y则不需要评估where 。xtrue

评估顺序以前在 C 和 C++ 中使用序列点定义,并且两者都更改了术语以根据排序前的关系来定义事物。有关详细信息,请参阅未定义的行为和序列点

于 2013-12-24T23:34:28.620 回答
10

C 标准中运算符的优先级由语法指示。

(C99,6.5p3)“运算符和操作数的分组由语法指示。74)”

74)“语法指定运算符在表达式求值中的优先级”

C99 基本原理还说

“优先规则被编码到每个运算符的句法规则中。”

“关联性规则类似地编码到句法规则中。”

另请注意,关联性与评估顺序无关。在:

f1() * f2() + f3()

函数调用以任何顺序进行评估。C 语法规则说这f1() * f2() + f3()意味着(f1() * f2()) + f3(),但表达式中操作数的评估顺序是未指定的。

于 2013-12-24T23:32:31.483 回答
8

考虑优先级和关联性的一种方法是想象该语言只允许包含赋值和一个运算符的语句,而不是多个运算符。所以像这样的声明:

a = f1() * f2() + f3();

不允许,因为它有 5 个运算符:3 个函数调用、乘法和加法。在这种简化的语言中,您必须将所有内容分配给临时对象,然后将它们组合起来:

temp1 = f1();
temp2 = f2();
temp3 = temp1 * temp2;
temp4 = f3();
a = temp3 + temp4;

关联性和优先级指定最后两个语句必须按该顺序执行,因为乘法的优先级高于加法。但它没有指定前 3 个语句的相对顺序;这样做同样有效:

temp4 = f3();
temp2 = f2();
temp1 = f1();
temp3 = temp1 * temp2;
a = temp3 + temp4;

sftrabbit 举了一个例子,其中函数调用运算符的关联性是相关的:

a = f()();

当像上面那样简化它时,它变成:

temp = f();
a = temp();
于 2013-12-24T23:41:54.690 回答
4

标准中定义了优先级和关联性,它们决定了如何构建语法树。优先级由运算符类型(1+2*3is1+(2*3)和 not (1+2)*3)起作用,关联性由运算符位置(1+2+3is(1+2)+3和 not 1+(2+3))起作用。

评估顺序不同 - 它没有定义如何构建语法树 - 它定义了evaluate语法树中运算符节点的方式。评估顺序被定义为未定义 - 您永远不能依赖它,因为编译器可以自由选择他们认为合适的任何顺序。这样做是为了让编译器可以尝试优化代码。这个想法是程序员编写的代码不应该受到评估顺序的影响,并且无论顺序如何都会产生相同的结果。

于 2013-12-24T23:40:36.303 回答
3

从左到右的关联性意味着f() - g() - h(),仅此(f() - g()) - h()而已。假设f返回1。假设g返回2。假设h返回3。从左到右的关联性意味着结果是(1 - 2) - 3, or -4:编译器仍然允许首先调用gand h,这与关联性无关,但不允许给出 的结果1 - (2 - 3),这将是完全不同的事情。

于 2013-12-24T23:33:37.223 回答