运算符
1. 运算符优先级和结合性
运算符优先级和结合性表:
-
优先级 1 是最高优先级,17 级是最低优先级。具有较高优先级的运算符的操作数首先分组。
-
L->R 表示从左到右的关联性
-
R->L 表示从右到左的关联性
优先级 | 运算符 | 描述 | 示例 |
---|---|---|---|
1 L->R | :: :: |
全局作用域 (一元) 命名空间作用域 (二元) |
::name class_name::member_name |
2 L->R | () () type() type{} [] . -> ++ –– typeid const_cast dynamic_cast reinterpret_cast static_cast sizeof… noexcept alignof |
括号 函数调用 功能转换 列表初始化临时对象 (C++11) 数组下标 成员访问(对象) 成员访问(对象指针) 后置递增 后置递减 运行时类型信息 去除常量 运行时类型检查转换 类型转换 编译时类型检查转换 获取参数包大小 编译时异常检查 获取类型对齐方式 |
(expression) function_name(arguments) type(expression) type{expression} pointer[expression] object.member_name object_pointer->member_name lvalue++ lvalue–– typeid(type) 或 typeid(expression) const_cast dynamic_cast reinterpret_cast static_cast sizeof…(expression) noexcept(expression) alignof(type) |
3 R->L | + - ++ –– ! not ~ (type) sizeof co_await & * new new[] delete delete[] |
一元加 一元减 前置递增 前置递减 逻辑非 逻辑非 按位取反 C风格类型转换 字节大小 等待异步调用 取地址 解引用 动态内存分配 动态数组分配 动态内存删除 动态数组删除 |
+expression -expression lvalue ––lvalue !expression not expression ~expression (new_type)expression sizeof(type) 或 sizeof(expression) co_await expression (C20) &lvalue *expression new type new type[expression] delete pointer delete[] pointer |
4 L->R | ->* .* |
成员指针选择器 成员对象选择器 |
object_pointer->*pointer_to_member object.*pointer_to_member |
5 L->R | * / % |
乘法 除法 余数 |
expression * expression expression / expression expression % expression |
6 L->R | + - |
加法 减法 |
expression + expression expression - expression |
7 L->R | << >> |
按位左移 / 插入 按位右移 / 提取 |
expression << expression expression >> expression |
8 L->R | <=> | 三向比较 (C++20) | expression <=> expression |
9 L->R | < <= > >= |
小于比较 小于等于比较 大于比较 大于等于比较 |
expression < expression expression <= expression expression > expression expression >= expression |
10 L->R | == != |
等于 不等于 |
expression == expression expression != expression |
11 L->R | & | 按位与 | expression & expression |
12 L->R | ^ | 按位异或 | expression ^ expression |
13 L->R | | | 按位或 | expression | expression |
14 L->R | && and |
逻辑与 逻辑与 |
expression && expression expression and expression |
15 L->R | | or |
逻辑或 逻辑或 |
expression | expression expression or expression |
16 R->L | throw co_yield ?: = *= /= %= += -= <<= >>= &= |= ^= |
抛出表达式 生成表达式 (C++20) 条件表达式 赋值 乘法赋值 除法赋值 余数赋值 加法赋值 减法赋值 按位左移赋值 按位右移赋值 按位与赋值 按位或赋值 按位异或赋值 |
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 | , | 逗号运算符 | expression, expression |
C++ 不包含用于求幂的运算符(
operator^
在 C++ 中具有不同的功能)
1 |
|
上面这个程序使用Clang编译器和使用GCC编译器的结果不同,如果使用GCC会输出如下结果:
1 | Enter an integer:1 2 3 |
lang 编译器按从左到右的顺序计算参数。 GCC 编译器按从右到左的顺序计算参数。
操作数、函数参数和子表达式可以按任何顺序求值。
2. 算术运算符
2.1 一元算术运算符
有两个一元算术运算符:正号(+)和负号(-)。一元运算符是只接受一个操作数的运算符。
运算符 | 符号 | 形式 | 操作 |
---|---|---|---|
一元正号 | + | +x | x 的值 |
一元负号 | - | -x | x 的负值 |
2.2 二元算术运算符
有 5 个二进制算术运算符。二元运算符是接受左操作数和右操作数的运算符。
运算符 | 符号 | 形式 | 操作 |
---|---|---|---|
加法 | + | x + y | x 加 y |
减法 | - | x - y | x 减 y |
乘法 | * | x * y | x 乘以 y |
除法 | / | x / y | x 除以 y |
求余 | % | x % y | x 除以 y 的余数 |
浮点除法会返回小数值,整数除法会删除所有小数返回整数值。
我们可以使用static_cast<>
将整数转换为浮点数,这样我们就可以进行浮点除法而不是整数除法。考虑以下代码:
1 |
|
输出:
1 | int / int = 1 |
除数为0的整数除法会导致未定义行为,因为这个运算在数学上没有定义
2.3 算术赋值运算符
运算符 | 符号 | 形式 | 操作 |
---|---|---|---|
加法赋值 | += | x += y | 将 y 加到 x 上 |
减法赋值 | -= | x -= y | 从 x 中减去 y |
乘法赋值 | *= | x *= y | 将 x 乘以 y |
除法赋值 | /= | x /= y | 将 x 除以 y |
求余赋值 | %= | x %= y | 将 x 除以 y 的余数赋值给 x |
2.4 修改和非修改运算符
可以修改其操作数之一的值的运算符非正式地称为修改运算符。在 C++ 中,大多数运算符都是非修改性的 – 它们只使用操作数来计算并返回一个值。但是,有两类内置运算符会修改其左操作数(并返回值):
-
赋值运算符,包括标准赋值运算符 (
=
)、算术赋值运算符(+=
、-=
、*=
、/=
和%=
)和按位赋值运算符(<<=
、>>=
、&=
、|=
和^=
) -
递增和递减运算符(分别为
++
和--
)
3. 余数和幂
3.1 余数
余数运算符(通常也称为模运算符)是在进行整数除法后返回余数的运算符。这对于测试一个数字是否能被另一个数字整除(意味着除法后没有余数)最有用:如果 x % y 的计算结果为 0,那么我们知道 x 能被 y 整除。
1 |
|
以下是运行示例:
1 | Enter an integer: 6 |
1 | Enter an integer: 6 |
余数采用第一个操作数的符号
如果第一个操作数可以为负数,则必须注意余数也可以为负数。例如,你可能会考虑编写一个函数来返回一个数字是否为奇数,如下所示:
1 | bool isOdd(int x) |
但是,当 x 是负奇数(例如-5
时,此操作将会失败,因为-5 % 2
是 -1,并且 -1 != 1。因此,如果要比较余数运算的结果,最好与 0 进行比较,因为 0 不存在正数/负数问题:
1 | bool isOdd(int x) |
3.2 幂
要在 C++ 中计算指数,请 #include <cmath>
标头,并使用 pow()
函数(即使你传递的是整数或整数参数)。如果你想进行整数求幂,最好使用您自己的函数来执行此操作。以下函数实现整数求幂(为了提高效率,使用非直观的“平方求幂”算法):
1 |
|
在绝大多数情况下,整数求幂会溢出整数类型。这可能就是为什么这样的函数最初没有包含在标准库中的原因。
这是上面的求幂函数的一个更安全的版本,用于检查溢出:
1 |
|
4. 递增/递减运算符
变量的递增(加 1)和减(减 1)都非常常见,以至于它们都有自己的运算符。
运算符 | 符号 | 形式 | 运算 |
---|---|---|---|
前置自增(前自增) | ++ | ++x | 先将 x 增加 1,然后返回 x |
前置自减(前自减) | – | –x | 先将 x 减少 1,然后返回 x |
后置自增(后自增) | ++ | x++ | 先复制 x,然后将 x 增加 1,最后返回复制值 |
后置自减(后自减) | – | x– | 先复制 x,然后将 x 减少 1,最后返回复制值 |
这是一个显示前缀和后缀版本之间差异的示例:
1 |
|
输出:
1 | 5 5 |
首选前缀版本,因为它们的性能更高,不太可能引起意外。
5. 逗号运算符
运算符 | 符号 | 形式 | 操作 |
---|---|---|---|
逗号运算符 | , | x, y | 先计算 x,再计算 y,返回 y 的值 |
逗号运算符 (,) 允许你在允许使用单个表达式的情况下计算多个表达式。逗号运算符先计算左操作数,然后计算右操作数,最后返回右操作数的结果。示例:
1 |
|
首先计算逗号运算符的左操作数,将_x_从_1_增加到_2_ 。接下来,计算右操作数,将y从2增加到3 。逗号运算符返回右操作数 ( 3 ) 的结果,随后将其打印到控制台。
请注意,逗号在所有运算符中的优先级最低,甚至低于赋值。因此,以下两行代码执行不同的操作:
1 | z = (a, b); // 首先计算 (a, b) 以获得 b 的结果,然后将该值分配给变量 z。 |
几乎在每种情况下,使用逗号运算符编写的语句最好编写为单独的语句。避免使用逗号运算符,除非在for 循环中
在 C++ 中,逗号符号经常用作分隔符,并且这些用途不会调用逗号运算符。分隔符逗号的一些示例:
1 | void foo(int x, int y) // Separator comma used to separate parameters in function definition |
6. 条件运算符
运算符 | 符号 | 形式 | 含义 |
---|---|---|---|
条件运算符 | ?: | c ? x : y | 如果条件 c 为真,则计算 x,否则计算 y |
条件运算符( ?:
)(有时也称为算术 if运算符)是三元运算符(采用 3 个操作数的运算符)。因为它历来是 C++ 唯一的三元运算符,所以有时也称为“三元运算符”。?:
运算符提供了执行特定类型的 if-else 语句的简写方法。
if-else 语句采用以下形式:
1 | if (condition) |
?:
运算符采用以下形式:
1 | condition ? expression1 : expression2; |
如果condition
计算结果为true
,则计算expression1
,否则计算expression2
。 :
和expression2
是可选的。
一个示例:
1 |
|
首先,让我们输入 5
和 7
作为输入(所以 x
是 5,y
是 ```7)。初始化
max时,将计算表达式
(5 > 7) ? 5 : 7。由于
5 > 7为 false,因此会生成
false ? 5 : 7,其计算结果为
7`。程序将打印:
1 | The max of 5 and 7 is 7. |
由于条件运算符作为表达式的一部分进行计算,因此可以在接受表达式的任何地方使用它。当条件运算符的操作数是常量表达式时,可以在常量表达式中使用条件运算符。这使得条件运算符可以用在不能使用语句的地方。示例:
1 |
|
由于 C++ 将大多数运算符的求值优先于条件运算符的求值,因此使用条件运算符编写的表达式很容易无法按预期求值。为了避免此类优先级问题,条件运算符应加括号。示例:
1 | return isStunned ? 0 : movesLeft; // not used in compound expression, condition contains no operators |
表达式的类型必须匹配或可转换:
-
第二个和第三个操作数的类型必须匹配。
-
编译器必须能够找到一种方法将第二个和第三个操作数中的一个或两个转换为匹配类型。编译器使用的转换规则相当复杂,在某些情况下可能会产生令人惊讶的结果。示例:
1 |
|
如果编译器找不到将第二个和第三个操作数转换为匹配类型的方法,则会导致编译错误:
1 |
|
在这种情况下,您可以执行显式转换,或使用 if-else 语句。
条件运算符在执行以下操作之一时最有用:
-
使用两个值之一初始化对象。
-
将两个值之一分配给对象。
-
将两个值之一传递给函数。
-
从函数返回两个值之一。
-
打印两个值之一。
复杂的表达式通常应避免使用条件运算符,因为它们容易出错且难以阅读。
7. 关系运算符和浮点比较
关系运算符是允许你比较两个值的运算符。有 6 个关系运算符:
运算符 | 符号 | 形式 | 运算 |
---|---|---|---|
大于 | > | x > y | 如果 x 大于 y 则返回 true,否则返回 false |
小于 | < | x < y | 如果 x 小于 y 则返回 true,否则返回 false |
大于等于 | >= | x >= y | 如果 x 大于等于 y 则返回 true,否则返回 false |
小于等于 | <= | x <= y | 如果 x 小于等于 y 则返回 true,否则返回 false |
等于 | == | x == y | 如果 x 等于 y 则返回 true,否则返回 false |
不等于 | != | x != y | 如果 x 不等于 y 则返回 true,否则返回 false |
默认情况下, if 语句_或_条件运算符(以及其他一些地方)中的条件计算为布尔值,正确的表达:
1 | if (b1) ... |
多余的表达:
1 | if (b1 == true) ... |
使用任何关系运算符比较浮点值都可能很危险。这是因为浮点值并不精确,浮点操作数中的小舍入误差可能会导致它们比预期稍小或稍大。避免使用运算符 ==
和运算符!=
来比较浮点值。示例:
1 |
|
变量 d1 和 d2 的值都应该为 0.01。但是这个程序打印了一个意外的结果:
1 | d1 > d2 |
将浮点文字与已使用相同类型的文字初始化的相同类型的变量进行比较是安全的,只要每个文字中的有效位数不超过该类型的最小精度。 Float 的最小精度为 6 位有效数字,double 的最小精度为 15 位有效数字。比较不同类型的浮点文字通常是不安全的。
实现浮点相等的最常见方法是使用一个函数来查看两个数字是否_几乎_相同。如果它们“足够接近”,那么我们称它们相等。用于表示“足够接近”的值传统上称为epsilon 。 Epsilon 通常被定义为一个小的正数(例如 0.00000001,有时写为 1e-8)。示例:
1 |
|
这个方法意味着每次我们调用这个函数时,我们都必须选择一个适合我们输入的 epsilon。著名计算机科学家Donald Knuth在他的著作《计算机编程的艺术,第二卷:半数值算法》(Addison-Wesley,1969)中提出了以下方法:
1 |
|
在这种情况下,epsilon 不再是绝对数,而是相对于a或b的大小。
-
在 <= 运算符的左侧,
std::abs(a - b)
以正数形式告诉我们a和b之间的距离。 -
在 <= 运算符的右侧,我们需要计算我们愿意接受的“足够接近”的最大值。为此,算法选择a和b中较大的一个(作为数字总体大小的粗略指标),然后将其乘以 relEpsilon。在此函数中,relEpsilon 代表百分比。例如,如果我们想说“足够接近”意味着a和b与a和b中较大者的误差在 1% 以内,则我们传入 relEpsilon 0.01 (1% = 1/100 = 0.01)。 relEpsilon 的值可以调整为最适合具体情况的值(例如,epsilon 0.002 表示误差在 0.2% 以内)。
虽然 approxEqualRel()
函数适用于大多数情况,但它并不完美,特别是当数字接近零时:
1 |
|
结果:
1 | true |
浮点数的比较是一个困难的话题,并且没有适用于所有情况的“一刀切”算法。但是,
absEqualAbsRel()
函数(absEpsilon
为 1e-12,relEpsilon
为 1e-8)应该足以处理我们遇到的大多数情况。
8. 逻辑运算符
逻辑运算符为我们提供了测试多个条件的能力。C++ 有 3 个逻辑运算符:
运算符 | 符号 | 示例用法 | 运算 |
---|---|---|---|
逻辑非 | ! | !x | 如果 x 为假则返回 true,如果 x 为真则返回 false |
逻辑与 | && | x && y | 如果 x 和 y 都为真则返回 true,否则返回 false |
逻辑或 | || | x || y | 如果 x 或 y(或两者都)为真则返回 true,否则返回 false |
8.1 逻辑非(NOT)
逻辑非经常用于条件语句中:
1 | bool tooLarge { x > 100 }; // tooLarge is true if x > 100 |
逻辑非具有非常高的优先级。如果逻辑非旨在对其他运算符的结果进行操作,则需要将其他运算符及其操作数括在括号中。例如
!(x>y)
8.2 逻辑或(OR)
逻辑或运算符用于测试两个条件中的任何一个是否为 true
如果要将一个变量与多个值进行比较,则需要多次比较该变量:
1 | if (value == 0 || value == 1) // correct: if value is 0, or if value is 1 |
错误写法:
1 | if (value == 0 || 1) // incorrect: if value is 0, or if 1 |
当计算1
时,它将隐式转换为bool
true
。因此,这个条件将始终评估为true
。
你可以将许多逻辑或语句串在一起:
1 | if (value == 0 || value == 1 || value == 2 || value == 3) |
8.3 逻辑与(AND)
逻辑与运算符用于测试两个作数是否为 true。例如,我们可能想知道变量 x 的值是否在 10 和 20 之间。这实际上是两个条件:我们需要知道 x 是否大于 10,以及 x 是否小于 20。
1 |
|
与逻辑或一样,您可以将许多逻辑与语句串在一起:
1 | if (value > 10 && value < 20 && value != 16) |
8.4 短路求值
为了让逻辑与运算返回真(true),两个操作数都必须被评估为真。如果左边的操作数评估为假(false),逻辑与运算符就知道无论右边的操作数评估为真还是假,它都必须返回假。在这种情况下,逻辑与运算符会立即返回假,甚至不会去计算右边的操作数!这被称为短路求值(short circuit evaluation),主要是为了优化目的。同样,如果逻辑或的左操作数为 true,则整个 OR 条件必须计算为 true,并且不会计算右操作数。
短路求值提供了另一个机会来说明为什么不应该在复合表达式中使用引起副作用的运算符。请考虑以下代码段:
1 | if (x == 1 && ++y == 2) |
如果x不等于1 ,则整个条件必定为 false,因此 ++y
永远不会被计算!因此,只有当x计算结果为 1 时, y才会递增,这可能不是程序员的意图!
8.5 混合逻辑与和逻辑非
逻辑与和逻辑或看起来像一对,但是逻辑与的优先级高于逻辑或 ,因此逻辑与运算符将先于逻辑或运算符进行计算(除非它们已被括号括起来)。因此在单个表达式中混合使用逻辑与和逻辑非时,请显式将每个操作用括号括起来,以确保它们正确按预期进行计算
8.6 德摩根定律
德摩根定律:
-
!(x && y)
相当于!x || !y
-
!(x || y)
等价于!x && !y
8.7 逻辑异或(XOR)
C++ 不提供显式逻辑异或运算符( operator^
是按位异或,而不是逻辑异或)。与逻辑或或逻辑与不同,逻辑异或不能进行短路求值。因此,用逻辑或和逻辑与运算符创建逻辑异或运算符具有挑战性。逻辑异或可以实现如下:
1 | if (a != b) ... // a XOR b, assuming a and b are bool |
这可以扩展到多个操作数,如下所示:
1 | if (a != b != c) ... // a XOR b XOR c, assuming a, b, and c are bool |
如果操作数不是bool
类型,则使用operator!=
实现逻辑异或将无法按预期工作。如果您需要一种适用于非布尔操作数的逻辑异或形式,您可以将操作数 static_cast
为 bool:
1 | if (static_cast<bool>(a) != static_cast<bool>(b) != static_cast<bool>(c)) ... // a XOR b XOR c, for any type that can be converted to bool |
下面的技巧也有效,而且更加简洁:
1 | if (!!a != !!b != !!c) // a XOR b XOR c, for any type that can be converted to bool |
这利用了operator!
(逻辑 NOT 运算符)将其操作数隐式转换为bool
C++ 中的许多运算符(例如运算符 ||)的名称只是符号。从历史上看,并非所有键盘和语言标准都支持键入这些运算符所需的所有符号。因此,C++ 支持使用单词而不是符号的运算符的替代关键字集。例如,代替||
,您可以使用关键字or
。
Primary | Alternative |
---|---|
&& |
and |
&= |
and_eq |
& |
bitand |
| |
bitor |
~ |
compl |
! |
not |
!= |
not_eq |
| |
or |
|= |
or_eq |
^ |
xor |
^= |
xor_eq |
虽然这些替代名称现在看起来可能更容易理解,但大多数经验丰富的 C++ 开发人员更喜欢使用符号名称而不是关键字名称。