算术运算符
返回特定算术运算的结果。
运算符名 | 语法 | 可重载 | 原型示例(对于 class T) | |
---|---|---|---|---|
类内定义 | 类外定义 | |||
一元加 | +a
|
是 | T T::operator+() const; | T operator+(const T &a); |
一元减 | -a
|
是 | T T::operator-() const; | T operator-(const T &a); |
加法 | a + b
|
是 | T T::operator+(const T2 &b) const; | T operator+(const T &a, const T2 &b); |
减法 | a - b
|
是 | T T::operator-(const T2 &b) const; | T operator-(const T &a, const T2 &b); |
乘法 | a * b
|
是 | T T::operator*(const T2 &b) const; | T operator*(const T &a, const T2 &b); |
除法 | a / b
|
是 | T T::operator/(const T2 &b) const; | T operator/(const T &a, const T2 &b); |
模 | a % b
|
是 | T T::operator%(const T2 &b) const; | T operator%(const T &a, const T2 &b); |
逐位非 | ~a
|
是 | T T::operator~() const; | T operator~(const T &a); |
逐位与 | a & b
|
是 | T T::operator&(const T2 &b) const; | T operator&(const T &a, const T2 &b); |
逐位或 | a | b
|
是 | T T::operator|(const T2 &b) const; | T operator|(const T &a, const T2 &b); |
逐位异或 | a ^ b
|
是 | T T::operator^(const T2 &b) const; | T operator^(const T &a, const T2 &b); |
逐位左移 | a << b
|
是 | T T::operator<<(const T2 &b) const; | T operator<<(const T &a, const T2 &b); |
逐位右移 | a >> b
|
是 | T T::operator>>(const T2 &b) const; | T operator>>(const T &a, const T2 &b); |
|
解释
所有算术运算符计算特定算术运算的结果,并返回它的结果。不修改实参。
转换
如果传递给算术运算符的操作数是整数或无作用域枚举类型,那么在所有其他操作前(但在左值到右值转换后,如果适用)会对操作数实施整数提升。如果操作数之一具有数组或函数类型,那么实施数组到指针和函数到指针转换。
对于(除移位之外的)二元运算符,当提升后的操作数拥有不同类型时,实施一般算术转换。
溢出
无符号整数算术始终进行 modulo 2n
,其中 n 是该整数的位数。例如对于 unsigned int,向 UINT_MAX 加一得到 0,而从 0 减一得到 UINT_MAX。
有符号整数算术运算溢出(结果类型无法容纳它的结果)时,行为未定义,此类操作可能会表现为:
- 按照表示法的规则(典型为补码)发生回绕,
- 在某些平台上或由于编译器选项(例如 GCC 和 Clang 中的
-ftrapv
)引发陷阱,
- 在某些时候截断到最小或最大值(在许多 DSP 上)
- 完全被编译器优化掉。
浮点环境
如果支持
#pragma STDC FENV_ACCESS 并设置它为 ON
,那么所有浮点算术运算符都服从当前浮点舍入方向,并报告 math_errhandling 中指定的浮点算术错误,除非在静态初始化器中发生(此时不引发浮点异常,且舍入模式是最近舍入)。
浮点缩略
除非支持
#pragma STDC FP_CONTRACT 并设置它为 OFF
,否则所有浮点算术都可以如同它的中间结果拥有无限范围和精度一般进行,即允许实施省略舍入误差和浮点异常的优化。例如 C++ 允许以单条融合乘加 CPU 指令来实现 (x * y) + z,或把 a = x * x * x * x; 优化为 tmp = x * x; a = tmp * tmp。
无关乎缩略,浮点算术的中间结果可拥有与它的类型所指定不同的范围和精度,见 FLT_EVAL_METHOD。
正式来讲,C++ 标准不在浮点运算的精度上做任何保证。
一元算术运算符
一元算术运算符表达式的形式为
+ 表达式
|
(1) | ||||||||
- 表达式
|
(2) | ||||||||
内建的一元加运算符返回它的操作数的值。仅有的非无操作情形,是当操作数拥有整数类型或无作用域枚举类型时,它将被整数提升所更改,例如将 char 转换到 int,或者当操作数服从左值到右值、数组到指针或函数到指针转换时。
内建的一元减运算符计算它提升后操作数的相反数。对于无符号的 a
,-a
的值是 2b
-a,其中 b
是提升后的位数。
在针对用户定义运算符的重载决议中,对于每个无 cv 限定的提升后算术类型 A
和每个类型 T
,下列函数签名参与重载决议:
A operator+(A) |
||
T* operator+(T*) |
||
A operator-(A) |
||
#include <iostream> int main() { char c = 0x6a; int n1 = 1; unsigned char n2 = 1; unsigned int n3 = 1; std::cout << "char:" << c << " int:" << +c << '\n' << "-1,当 1 的类型是 signed 时:" << -n1 << '\n' << "-1,当 1 的类型是 unsigned char 时:" << -n2 << '\n' << "-1,当 1 的类型是 unsigned int 时:" << -n3 << '\n'; char a[3]; std::cout << "数组大小:" << sizeof a << '\n' << "指针大小:" << sizeof +a << '\n'; }
输出:
char:j int:106 -1,当 1 的类型是 signed 时:-1 -1,当 1 的类型是 unsigned char 时:-1 -1,当 1 的类型是 unsigned int 时:4294967295 数组大小:3 指针大小:8
加性运算符
二元加性算术运算符表达式的形式为
左操作数 + 右操作数
|
(1) | ||||||||
左操作数 - 右操作数
|
(2) | ||||||||
- 都具有算术或无作用域枚举类型。这种情况下,对两个操作数都实施一般算术转换,并确定结果类型。
- 一个具有指向完整定义的对象的指针类型,另一个具有整数或无作用域枚举类型。此时结果具有该指针的类型。
- 都具有算术或无作用域枚举类型。这种情况下,对两个操作数都实施一般算术转换,并确定结果类型。
- 左操作数 是指向完整定义的对象类型的指针,右操作数 具有整数或无作用域枚举类型。此时结果具有该指针的类型。
- 忽略 cv 限定符时,两者都是指向相同完整定义的对象类型的指针。此时结果类型是 std::ptrdiff_t。
对于算术或枚举类型的操作数,二元加的结果是操作数(经一般算术转换后)的和,而二元减运算符的结果是第一操作数减去第二操作数(经一般算术转换后)的结果,但是,如果该类型支持 IEEE 浮点算术(见 std::numeric_limits::is_iec559),
- 如果操作数之一是 NaN,那么结果是 NaN
- 无穷减无穷是 NaN,并引发 FE_INVALID
- 无穷加负无穷是 NaN,并引发 FE_INVALID
如果有任何操作数是指针,那么适用下列规则:
- 把指向非数组对象的指针当做指向大小为 1 的数组的首元素的指针。
- 如果指针 P 指向数组的第
i
个元素,那么表达式 P + n、n + P 和 P - n 分别是指向同一数组的第i+n
、第i+n
和第i-n
个元素的同类型指针。指针加法的结果也可以是末尾后一位置(one past the end)的指针(即使得 P - 1 指向数组末元素的指针 P)。其他所有情形(即试图生成不指向同一数组的任何元素或同一数组末尾后一位置的指针)均引起未定义行为。 - 如果指针 P 指向数组的第
i
个元素,而指针 Q 指向同一数组的第j
个元素,那么表达式 P - Q 拥有值 i - j(如果 std::ptrdiff_t 可以容纳该值)。两个操作数都必须指向同一数组的元素(或末尾后一位置),否则行为未定义。如果结果不能被 std::ptrdiff_t 容纳,那么行为未定义。 - 任何情况下,如果所指向的类型与数组元素类型不同(其中对于每一层元素,如果它们自身就是指针,那么不考虑它们的 cv 限定性),那么指针算术的行为未定义。特别是,以指向某个派生对象数组中的元素的指向基类指针做指针算术是未定义的。
- 对指针加或减 0,结果就是该指针,没有更改。如果两个指针指向同一对象,或都指向同一数组的末尾后一位置,或都是空指针,那么减法的结果等于 (std::ptrdiff_t)0。
这些指针算术运算符使得指针满足老式随机访问迭代器 (LegacyRandomAccessIterator) 的要求。
在针对用户定义运算符的重载决议中,对于每对提升后的算术类型 L
和 R
和每个对象类型 T
,下列函数签名参与重载决议:
LR operator+(L, R) |
||
LR operator-(L, R) |
||
T* operator+(T*, std::ptrdiff_t) |
||
T* operator+(std::ptrdiff_t, T*) |
||
T* operator-(T*, std::ptrdiff_t) |
||
std::ptrdiff_t operator-(T*, T*) |
||
其中 LR
是对 L
和 R
实施一般算术转换的结果。
#include <iostream> int main() { char c = 2; unsigned int un = 2; int n = -10; std::cout << " 2 + (-10),当 2 的类型是 char 时 = " << c + n << '\n' << " 2 + (-10),当 2 的类型是 unsigned 时 = " << un + n << '\n' << " -10 - 2.12 = " << n - 2.12 << '\n'; char a[4] = {'a', 'b', 'c', 'd'}; char* p = &a[1]; std::cout << "指针相加的例子:" << *p << *(p + 2) << *(2 + p) << *(p - 1) << '\n'; char* p2 = &a[4]; std::cout << "指针的差:" << p2 - p << '\n'; }
输出:
2 + (-10),当 2 的类型是 char 时 = -8 2 + (-10),当 2 的类型是 unsigned 时 = 4294967288 -10 - 2.12 = -12.12 指针相加的例子:bdda 指针的差:3
乘性运算符
二元乘性运算符表达式的形式为
左操作数 * 右操作数
|
(1) | ||||||||
左操作数 / 右操作数
|
(2) | ||||||||
左操作数 % 右操作数
|
(3) | ||||||||
这三个运算符均对两个操作数实施一般算术转换,并确定结果类型。
二元运算符 * 进行它的操作数(经一般算术转换后)的乘法,但对于浮点乘法,
- NaN 乘以任何数都是 NaN
- 无穷乘以零是 NaN 并引发 FE_INVALID
二元运算符 /(经一般算术转换后)将第一操作数除以第二操作数。
对于整数操作数,它产生代数商。商向零取整(舍弃小数部分)。
如果第二操作数为零,那么行为未定义,但如果进行浮点除法且类型支持 IEEE 浮点算术(见 std::numeric_limits::is_iec559),那么:
- 如果操作数之一是 NaN,那么结果是 NaN
- 非零数除以 ±0.0 会得到符号正确的无穷,并引发 FE_DIVBYZERO
- 0.0 除以 0.0 是 NaN,并引发 FE_INVALID
二元运算符 % 产生以第二操作数整除第一操作数的余数(经一般算术转换后;注意各操作数的类型必须都是整数类型)。如果商 a / b 能以结果类型表示,那么 (a / b) * b + a % b == a。如果第二操作数为零,那么行为未定义。如果商 a / b 不能以结果类型表示,那么 a / b
和 a % b
的行为均未定义(这意味着,补码系统上 INT_MIN % -1 未定义)。
注意:在 CWG 问题 614 解决(N2757)之前,如果二元运算符 % 的一或两个操作数为负,那么余数符由实现定义,因为它取决于整数除法的舍入方向。函数 std::div 在该情况下提供良好定义的行为。
注意:对于浮点余数,见 std::remainder 和 std::fmod。
在针对用户定义运算符的重载决议中,对于每对提升后的算术类型 LA
与 RA
及每对提升后的整数类型 LI
与 RI
,下列函数签名参与重载决议:
LRA operator*(LA, RA) |
||
LRA operator/(LA, RA) |
||
LRI operator%(LI, RI) |
||
其中 LRx
是对 Lx
和 Rx
实施一般算术转换的结果。
#include <iostream> int main() { char c = 2; unsigned int un = 2; int n = -10; std::cout << "2 * (-10),当 2 的类型是 char 时 = " << c * n << '\n' << "2 * (-10),当 2 的类型是 unsigned 时 = " << un * n << '\n' << "-10 / 2.12 = " << n / 2.12 << '\n' << "-10 / 21 = " << n / 21 << '\n' << "-10 % 21 = " << n % 21 << '\n'; }
输出:
2 * (-10),当 2 的类型是 char 时 = -20 2 * (-10),当 2 的类型是 unsigned 时 = 4294967276 -10 / 2.12 = -4.71698 -10 / 21 = 0 -10 % 21 = -10
逐位逻辑运算符
逐位算术运算符表达式的形式为
~ 右操作数
|
(1) | ||||||||
左操作数 & 右操作数
|
(2) | ||||||||
左操作数 | 右操作数
|
(3) | ||||||||
左操作数 ^ 右操作数
|
(4) | ||||||||
operator~ 的结果是实参值(提升后)的逐位非(所有零位变成一位,所有一位变成零位)。operator& 的结果是两个操作数值(经一般算术转换后)的逐位与。operator| 的结果是两个操作数值(经一般算术转换后)的逐位或。operator^ 的结果是两个操作数值(经一般算术转换后)的逐位异或。
~ 在后随类型名或 decltype 说明符 (C++11 起)时在语法上有歧义:它可以是 operator~,也可以是析构函数标识符的开始。该歧义通过将 ~ 视为 operator~ 解决。只有在不能组成 operator~ 的地方才能用 ~ 作为析构函数标识符的开始。
在针对用户定义运算符的重载决议中,对于每对提升后的整数类型 L
和 R
,下列函数签名参与重载决议:
R operator~(R) |
||
LR operator&(L, R) |
||
LR operator^(L, R) |
||
LR operator|(L, R) |
||
其中 LR
是对 L
和 R
实施一般算术转换的结果。
#include <iostream> #include <iomanip> #include <bitset> int main() { uint16_t mask = 0x00f0; uint32_t x0 = 0x12345678; uint32_t x1 = x0 | mask; uint32_t x2 = x0 & ~mask; uint32_t x3 = x0 & mask; uint32_t x4 = x0 ^ mask; uint32_t x5 = ~x0; using bin16 = std::bitset<16>; using bin32 = std::bitset<32>; std::cout << std::hex << std::showbase << "掩码:" << mask << std::setw(39) << bin16(mask) << '\n' << "值:" << x0 << std::setw(33) << bin32(x0) << '\n' << "设置各位:" << x1 << std::setw(33) << bin32(x1) << '\n' << "清除各位:" << x2 << std::setw(33) << bin32(x2) << '\n' << "选择各位:" << x3 << std::setw(39) << bin32(x3) << '\n' << "异或各位:" << x4 << std::setw(33) << bin32(x4) << '\n' << "取反各位:" << x5 << std::setw(33) << bin32(x5) << '\n'; }
输出:
掩码:0xf0 0000000011110000 值:0x12345678 00010010001101000101011001111000 设置各位:0x123456f8 00010010001101000101011011111000 清除各位:0x12345608 00010010001101000101011000001000 选择各位:0x70 00000000000000000000000001110000 异或各位:0x12345688 00010010001101000101011010001000 取反各位:0xedcba987 11101101110010111010100110000111
移位运算符
移位运算符表达式的形式为
左操作数 << 右操作数
|
(1) | ||||||||
左操作数 >> 右操作数
|
(2) | ||||||||
返回类型是提升后的左操作数的类型。
对于无符号 a,a << b 的值是 a * 2b 对于有符号的非负 a,若 a * 2b 对于负 a,a << b 的行为未定义。 对于无符号 a 和有符号的非负 a,a >> b 的值是 a/2b 对于负 a,a >> b 的值由实现定义(大多数平台上进行算术右移,因此结果保留为负)。 |
(C++20 前) |
a << b 的值是与 a * 2b a >> b 的值是 a/2b |
(C++20 起) |
任何情况下,如果右操作数的值为负或大于等于提升后左操作数中的位数,那么行为未定义。
在针对用户定义运算符的重载决议中,对于每对提升后的整数类型 L
和 R
,下列函数签名参与重载决议:
L operator<<(L, R) |
||
L operator>>(L, R) |
||
#include <iostream> enum { ONE = 1, TWO = 2 }; int main() { std::cout << std::hex << std::showbase; char c = 0x10; unsigned long long ull = 0x123; std::cout << "0x123 << 1 = " << (ull << 1) << '\n' << "0x123 << 63 = " << (ull << 63) << '\n' // 无符号中的溢出 << "0x10 << 10 = " << (c << 10) << '\n'; // 提升 char 到 int long long ll = -1000; std::cout << std::dec << "-1000 >> 1 = " << (ll >> ONE) << '\n'; }
输出:
0x123 << 1 = 0x246 0x123 << 63 = 0x8000000000000000 0x10 << 10 = 0x4000 -1000 >> 1 = -500
标准库
许多标准库类型都重载了算术运算符。
一元算术运算符
实现一元 + 和一元 - ( std::chrono::duration<Rep,Period> 的公开成员函数) | |
对复数运用一元运算符 (函数模板) | |
对 valarray 的每个元素运用一元算术运算符 ( std::valarray<T> 的公开成员函数) |
加性运算符
(C++11) |
实施涉及时间点的加法和减法运算 (函数模板) |
实现以时长为实参的算术运算 (函数模板) | |
(C++20) |
将 year_month_day 与一定量的年数或月数相加或相减 (函数) |
连接两个字符串或者一个字符串和一个字符 (函数模板) | |
推进或减小迭代器 ( std::reverse_iterator<Iter> 的公开成员函数)
| |
推进或减小迭代器 ( std::move_iterator<Iter> 的公开成员函数)
| |
在两个复数,或一个复数与一个标量上进行复数算术运算 (函数模板) | |
对两个 valarray 的每个元素,或一个 valarray 的每个元素和一个值运用二元运算符 (函数模板) |
乘性运算符
实现以时长为实参的算术运算 (函数模板) | |
在两个复数,或一个复数与一个标量上进行复数算术运算 (函数模板) | |
对两个 valarray 的每个元素,或一个 valarray 的每个元素和一个值运用二元运算符 (函数模板) |
逐位逻辑运算符
进行二进制与、或、异或及非 ( std::bitset<N> 的公开成员函数) | |
在 bitset 上执行二元逻辑操作 (函数) | |
对 valarray 的每个元素运用一元算术运算符 ( std::valarray<T> 的公开成员函数)
| |
对两个 valarray 的每个元素,或一个 valarray 的每个元素和一个值运用二元运算符 (函数模板) |
移位运算符
对两个 valarray 的每个元素,或一个 valarray 的每个元素和一个值运用二元运算符 (函数模板) | |
进行二进制左移和右移 ( std::bitset<N> 的公开成员函数)
|
流插入/提取运算符
整个标准库中,移位运算符常对输入/输出流(std::ios_base& 或从它派生的类之一)重载,以之为左运算数和返回类型。这种运算符被称为流插入和流抽取运算符:
提取带格式数据 ( std::basic_istream<CharT,Traits> 的公开成员函数) | |
提取字符和字符数组 (函数模板) | |
插入带格式数据 ( std::basic_ostream<CharT,Traits> 的公开成员函数) | |
插入字符数据 (函数) | |
复数的序列化和反序列化 (函数模板) | |
执行 bitset 的流输入和输出 (函数) | |
执行字符串的流输入与输出 (函数模板) | |
(C++11) |
执行伪随机数引擎的流输入和输出 (函数) |
(C++11) |
执行伪随机数分布的流输入和输出 (函数模板) |
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 614 | C++98 | 整数除法的算术商的取整方向由实现定义 | 向零取整(舍弃小数部分) |
CWG 1450 | C++98 | 未指明 a / b 在无法以结果类型表示时的结果 | 此时 a / b 和 a % b 的行为均未定义 |
CWG 1457 | C++98 | 将正的有符号值的最左端的 1 位移入符号位的行为未定义 | 赋予良好定义 |
CWG 1504 | C++98 | 指向数组元素的基类子对象的指针可以用于指针算术 | 此时行为未定义 |
CWG 1515 | C++98 | 只有声明了 unsigned 的无符号整数需要遵循 modulo 2n 算术规则 |
应用到所有无符号整数 |
CWG 1865 | C++98 | CWG 问题 1504 的解决方案导致指向数组元素的指针在指针指向 类型和数组类型的非顶层 cv 限定不同时参与指针运算的行为未定义 |
赋予良好定义 |
CWG 1971 | C++98 | 不明确用来解决 ~ 的歧义的规则是否适用 ~X(0) 这种情况 | 适用这种情况 |
CWG 2419 | C++98 | 只有通过 & 获取的到非数组对象的指针才会在 指针运算中被视为到大小为 1 的数组的首元素的指针 |
应用到所有到非数组对象的指针 |
CWG 2626 | C++98 | 内建 operator~ 的结果只是“反码”,而没有适当的定义 | 结果用二进制表示进行描述 |
参阅
常见运算符 | ||||||
---|---|---|---|---|---|---|
赋值 | 自增/自减 | 算术 | 逻辑 | 比较 | 成员访问 | 其他 |
a = b |
++a |
+a |
!a |
a == b |
a[b] |
函数调用 |
a(...) | ||||||
逗号 | ||||||
a, b | ||||||
条件 | ||||||
a ? b : c | ||||||
特殊运算符 | ||||||
static_cast 转换一个类型为另一相关类型 |