6-1 运算符优先级与结合性

章节导言

本章内容基于第1.9课——数值与运算符介绍中的概念展开。以下为简要回顾:

运算operation是一种数学过程,涉及零个或多个输入值(称为操作数operands),最终生成新值(称为输出值)。具体要执行的运算由称为运算符operator的构造(通常为符号或符号对)表示。

例如,孩童时期我们都学过2+3=5。在此例中,字面量2和3是操作数,符号+则是操作符,它指示我们对操作数执行数学加法运算以生成新值5。由于仅使用了一个操作符,该过程较为简单。

本章将探讨与运算符相关的主题,并深入解析C++支持的多种常用运算符。


复合表达式的求值

现在,我们来考虑一个复合表达式,例如 4 + 2 * 3。它应该被分组为 (4 + 2) * 3(求值为 18),还是 4 + (2 * 3)(求值为 10)?根据常规数学运算优先级规则(即乘法优先于加法),我们知道上述表达式应按 4 + (2 * 3) 进行分组,结果为 10。但编译器如何知道这一点?

为求解表达式,编译器需完成两项工作:

  • 编译时,编译器必须解析表达式并确定操作数与运算符的分组方式。这通过优先级和结合性规则实现,我们稍后将详细讨论。
  • 编译时或运行时,操作数将被求值,运算执行以得出结果。

运算符优先级

为协助解析复合表达式,所有运算符均被赋予优先级。优先级precedence更高的运算符将优先与操作数进行运算。

如下表所示,乘除运算(优先级5)高于加减运算(优先级6)。因此乘除运算将优先于加减运算与操作数组合。换言之,表达式4 + 2 * 3将被解析为4 + (2 * 3)。


运算符结合性

考虑一个复合表达式,如 7 - 4 - 1。它应该被分组为 (7 - 4) - 1(结果为 2),还是 7 - (4 - 1)(结果为 4)?由于两个减法运算符具有相同的优先级,编译器无法仅凭优先级来确定分组方式。

当两个同优先级的运算符在表达式中相邻时,运算符的结合性associativity决定了编译器应从左至右还是从右至左计算运算符(而非操作数!)。减法运算符的优先级为6,而优先级6的运算符具有从左到右的结合性。因此该表达式按从左到右的顺序分组:(7 - 4) - 1。


运算符优先级与结合性对照表

下表主要作为参考图表,供您今后查阅以解决任何优先级或结合性问题。

注释:

  • 优先级1为最高优先级,17为最低优先级。优先级越高的运算符,其操作数将优先进行分组。
  • L->R表示左向右结合性。
  • R->L表示右向左结合性。
Prec/Ass Operator Description Pattern
1 L→R ::
::
Global scope (unary)
Namespace scope (binary)
::name
class_name::member_name
2 L→R ()
()
type()
type{}
[]
.
->
++
--
typeid
const_cast
dynamic_cast
reinterpret_cast
static_cast
sizeof...
noexcept
alignof
Parentheses
Function call
Functional cast
List init temporary object (C++11)
Array subscript
Member access from object
Member access from object ptr
Post-increment
Post-decrement
Run-time type information
Cast away const
Run-time type-checked cast
Cast one type to another
Compile-time type-checked cast
Get parameter pack size
Compile-time exception check
Get type alignment
(expression)
function_name(arguments)
type(expression)
type{expression}
pointer[expression]
object.member_name
object_pointer->member_name
lvalue++
lvalue--
typeid(type) or typeid(expression)
const_cast<type>(expression)
dynamic_cast<type>(expression)
reinterpret_cast<type>(expression)
static_cast<type>(expression)
sizeof...(expression)
noexcept(expression)
alignof(type)
3 R→L +
-
++
--
!
not
~
(type)
sizeof
co_await
&
*
new
new[]
delete
delete[]
Unary plus
Unary minus
Pre-increment
Pre-decrement
Logical NOT
Logical NOT
Bitwise NOT
C-style cast
Size in bytes
Await asynchronous call
Address of
Dereference
Dynamic memory allocation
Dynamic array allocation
Dynamic memory deletion
Dynamic array deletion
+expression
-expression
++lvalue
--lvalue
!expression
not expression
~expression
(new_type)expression
sizeof(type) or sizeof(expression)
co_await expression (C++20)
&lvalue
*expression
new type
new type[expression]
delete pointer
delete[] pointer
4 L→R ->*
.*
Member pointer selector
Member object selector
object_pointer->*pointer_to_member
object.*pointer_to_member
5 L→R *
/
%
Multiplication
Division
Remainder
expression * expression
expression / expression
expression % expression
6 L→R +
-
Addition
Subtraction
expression + expression
expression - expression
7 L→R <<
>>
Bitwise shift left / Insertion
Bitwise shift right / Extraction
expression << expression
expression >> expression
8 L→R <=> Three-way comparison (C++20) expression <=> expression
9 L→R <
<=
>
>=
Comparison less than
Comparison less than or equals
Comparison greater than
Comparison greater than or equals
expression < expression
expression <= expression
expression > expression
expression >= expression
10 L→R ==
!=
Equality
Inequality
expression == expression
expression != expression
11 L→R & Bitwise AND expression & expression
12 L→R ^ Bitwise XOR expression ^ expression
13 L→R | Bitwise OR expression | expression
14 L→R &&
and
Logical AND
Logical AND
expression && expression
expression and expression
15 L→R ||
or
Logical OR
Logical OR
expression || expression
expression or expression
16 R→L throw
co_yield
?:
=
*=
/=
%=
+=
-=
<<=
>>=
&=
|=
^=
Throw expression
Yield expression (C++20)
Conditional
Assignment
Multiplication assignment
Division assignment
Remainder assignment
Addition assignment
Subtraction assignment
Bitwise shift left assignment
Bitwise shift right assignment
Bitwise AND assignment
Bitwise OR assignment
Bitwise XOR assignment
throw expression
co_yield expression
expression ? expression : expression
lvalue = expression
lvalue *= expression
lvalue /= expression
lvalue %= expression
lvalue += expression
lvalue -= expression
lvalue <<= expression
lvalue >>= expression
lvalue &= expression
lvalue |= expression
lvalue ^= expression
17 L→R , Comma operator expression, expression

你应该已经认识其中几个运算符,例如 +、-、*、/、() 和 sizeof。然而,除非你接触过其他编程语言,否则表格中大部分运算符对你而言目前可能难以理解。这在当前阶段是正常的。本章将讲解其中多数运算符,其余内容将在需要时逐步引入。

问:幂运算符在哪里?
C++未提供专门的幂运算符(^运算符在C++中具有不同功能)。我们在第6.3节——余数与幂运算中将详细讨论幂运算。

需注意:运算符operator<<同时处理位左移与字符串插入,运算符operator>>同时处理位右移与字符串提取。编译器会根据操作数的类型自动判断执行何种操作。


括号化

由于运算符优先级规则,4 + 2 * 3 将被分组为 4 + (2 * 3)。但如果我们实际想表达的是 (4 + 2) * 3 呢?正如常规数学运算,C++中可通过显式使用括号来设定运算数的分组方式。此机制得以实现是因为括号具有最高优先级之一,因此括号内的表达式通常优先于外部表达式进行求值。


使用括号使复合表达式更易理解

现在考虑表达式 x && y || z。它会按 (x && y) || z 还是 x && (y || z) 的顺序求值?你可以查阅运算符优先级表,发现 && 优先级高于 ||。但运算符种类繁多且优先级各异,难以全部记住。你也不希望每次都要查阅运算符优先级表来理解复合表达式的计算方式。

为减少错误并使代码更易理解(无需参考优先级表),建议对任何非简单的复合表达式添加括号,从而清晰传达你的意图。

最佳实践
使用括号明确非简单复合表达式的计算方式 (即使从技术上讲并非必要)。

经验法则是:除加减乘除运算外,其余表达式均应加括号。

上述最佳实践另有例外:仅含单个赋值运算符(且无逗号运算符)的表达式,其赋值操作数的右侧无需加括号。

例如:

x = (y + z + w);   // instead of this
x = y + z + w;     // it's okay to do this

x = ((y || z) && w); // instead of this
x = (y || z) && w;   // it's okay to do this

x = (y *= z); // expressions with multiple assignments still benefit from parenthesis

赋值运算符具有第二低的运算优先级(仅次于逗号运算符,而逗号运算符极少使用)。因此,只要存在单次赋值操作(且不包含逗号),我们就能确定右操作数将在赋值前完成完全求值。

最佳实践
包含单个赋值运算符的表达式中,无需将赋值操作的右操作数用括号包裹。


运算的值计算

C++标准使用“值计算value computation”一词来表示表达式中运算符的执行过程,以生成一个值。运算符的优先级和结合规则决定了值计算的顺序。

例如,表达式 4 + 2 * 3 根据运算符优先级规则将被分组为 4 + (2 * 3)。必须先计算 (2 * 3) 的值,才能完成 4 + 6 的值计算。


操作数的求值

C++标准(主要)使用“求值evaluation”一词指代操作数的求值过程(而非运算符或表达式的求值!)。例如,对于表达式 a + b,a 将被求值以产生某个值,b 也将被求值以产生某个值。这些值随后可作为+运算符的操作数用于值计算。

术语规范
非正式场合中,我们通常用“求值”表示对整个表达式的计算(即求值计算),而非仅指表达式中操作数的求值过程。


操作数(包括函数参数)的求值顺序大多未指定

在大多数情况下,操作数和函数参数的求值顺序未指定,这意味着它们可以按任意顺序求值。

考虑以下表达式:

a * b + c * d

根据上述优先级和结合律规则,我们知道该表达式将被分组,如同我们输入的是:

(a * b) + (c * d)

如果 a 是 1,b 是 2,c 是 3,d 是 4,那么这个表达式将始终计算出值 14。

然而,运算符优先级和结合律仅规定了运算符与操作数的分组方式及值计算的顺序,并未规定操作数或子表达式的求值顺序。编译器可自由选择求值操作数 a、b、c 或 d 的顺序,也可自由决定先计算 a * b 还是 c * d。

对于大多数表达式而言,这无关紧要。在上例中,变量 a、b、c 或 d 的求值顺序并不影响结果:计算值始终为 14,此处不存在歧义。

但某些表达式的求值顺序确实至关重要。请看这个程序,它包含了 C++ 新手常犯的错误:

#include <iostream>

int getValue()
{
    std::cout << "Enter an integer: ";

    int x{};
    std::cin >> x;
    return x;
}

void printCalculation(int x, int y, int z)
{
    std::cout << x + (y * z);
}

int main()
{
    printCalculation(getValue(), getValue(), getValue()); // this line is ambiguous

    return 0;
}

(这个是按照从左到右的顺序计算的, Clang)
image
(这个是按照从右到左的顺序计算的, g++)
image

若运行此程序并输入1、2、3,你可能会认为程序会计算1 + (2 * 3)并输出7。但这假设了printCalculation()的参数按从左到右顺序求值(即参数x先取值1,y取值2,z取值3)。若参数按从右到左的顺序求值(即参数 z 先取值 1,y 取值 2,x 最后取值 3),程序则会输出 5。

提示
Clang 编译器按从左到右的顺序求值实参。GCC 编译器按从右到左的顺序求值实参。

若想亲身体验这种行为,可在 Wandbox 上操作。将上述程序粘贴进去,在 Stdin 标签页输入 1 2 3,选择 GCC 或 Clang,然后编译程序。输出结果将显示在页面底部(可能需要向下滚动查看)。你会发现 GCC 和 Clang 的输出结果存在差异!

通过将每个 getValue() 函数调用拆分为独立语句,可消除上述程序的歧义:

#include <iostream>

int getValue()
{
    std::cout << "Enter an integer: ";

    int x{};
    std::cin >> x;
    return x;
}

void printCalculation(int x, int y, int z)
{
    std::cout << x + (y * z);
}

int main()
{
    int a{ getValue() }; // will execute first
    int b{ getValue() }; // will execute second
    int c{ getValue() }; // will execute third

    printCalculation(a, b, c); // this line is now unambiguous

    return 0;
}

在此版本中,a 将始终取值 1,b 将取值 2,c 将取值 3。当实参给 printCalculation() 的形参求值时,实参求值的顺序无关紧要——形参 x 将始终获得值 1,y 将获得值 2,z 将获得值 3。此版本将确定性地输出 7。

关键要点
操作数、函数参数和子表达式可按任意顺序求值。

常见误区是认为运算符优先级和结合性会影响求值顺序。优先级和结合性仅用于确定操作数如何与运算符分组,以及值计算的顺序。

警告
请确保所编写的表达式(或函数调用)不依赖于操作数(或参数)的求值顺序。

相关内容
具有副作用的运算符也可能导致意外的求值结果。本课程第6.4节——递增/递减运算符与副作用中将对此进行讲解。


测验时间

问题 #1

你从日常数学中知道,括号内的表达式会优先计算。例如,在表达式 (2 + 3) * 4 中,(2 + 3) 这部分会先被计算。

本练习中,你将获得一组不含括号的表达式。请参照上表中的运算符优先级和结合律规则,为每个表达式添加括号,明确编译器将如何计算该表达式。

显示提示

提示:使用上表中的模式列来判断运算符是一元运算符(具有一个操作数)还是二元运算符(具有两个操作数)。若需复习一元运算符和二元运算符的概念,请查阅第1.9课——字面量与运算符介绍

示例问题:x = 2 + 3 % 4

二进制运算符 % 的优先级高于 + 或 = 运算符,因此优先计算:

x = 2 + (3 % 4)

二进制运算符 + 的优先级高于 = 运算符,因此接着计算:

最终结果:x = (2 + (3 % 4))

现在我们无需参考上表即可理解表达式求值过程:

a) x = 3 + 4 + 5;

显示解答

二元运算符 + 的运算优先级高于 =:

x = (3 + 4 + 5);

二元运算符 + 具有从左到右的结合性:

最终结果:x = ((3 + 4) + 5);

b) x = y = z;

显示解答

二元运算符 = 具有从右到左的结合性:

最终答案:x = (y = z);

c) z *= ++y + 5;

显示解答

一元运算符 ++ 具有最高优先级:

z *= (++y) + 5;

二元运算符 + 具有次高优先级:

最终结果:z *= ((++y) + 5);

d) a || b && c || d;

显示解答

二元运算符 && 的优先级高于 ||:

a || (b && c) || d;

二元运算符 || 具有从左到右的结合性:

最终结果:(a || (b && c)) || d;
posted @ 2026-02-19 07:33  游翔  阅读(2)  评论(0)    收藏  举报