算术运算符
返回特定算术运算的结果。
运算符名 | 语法 | 可重载 | 原型示例(对于 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); |
|
解释
所有算术运算符计算特定算术运算的结果,并返回其结果。不修改实参。
转换
若传递给算术运算符的操作数是整型或无作用域枚举类型,则在任何其他行动前(但在左值到右值转换后,若其适用),对操作数实施整型提升。若操作数之一具有数组或函数类型,则实施数组到指针和函数到指针转换。
对于(除移位之外的)二元运算符,当提升后的操作数拥有不同类型时,实施称为一般算术转换 (usual arithmetic conversion)的一组额外的隐式转换,目的是产生公共类型 (common type)(亦可由 std::common_type 类型特性访问)。若在任何整型提升前,一个操作数为枚举类型而另一操作数为浮点类型或不同的枚举类型,则此行为被弃用。 (C++20 起)
- 若任一操作数具有有作用域枚举类型,则不进行转换:另一操作数和返回类型必须具有相同类型
- 否则,若任一操作数为 long double,则将另一操作数转换为 long double
- 否则,若任一操作数为 double,则将另一操作数转换为 double
- 否则,若任一操作数为 float,则将另一操作数转换为 float
- 否则,操作数拥有整数类型(因为 bool、char、char8_t、char16_t、char32_t、wchar_t 和无作用域枚举在此时已被提升),并对其实施整型转换以产生如下的公共类型:
- 若两个操作数均为有符号或均为无符号,则将拥有较小转换等级的操作数转换为拥有较大整数转换等级的操作数的类型。
- 否则,若无符号操作数的转换等级大于或等于有符号操作数的转换等级,则将有符号操作数转换为无符号操作数的类型。
- 否则,若有符号操作数的类型能表示无符号操作数类型的所有值,则将无符号操作数转换为有符号操作数的类型。
- 否则,将两个操作数都转换为有符号操作数的无符号对应版本。
转换等级(conversion rank)按 bool、signed char、short、int、long、long long 顺序递增。任何无符号类型的等级都等于其对应的有符号类型的等级。char 的等级等于 signed char 和 unsigned char 的等级。char8_t、char16_t、char32_t 和 wchar_t 的等级等于其底层类型的等级。
溢出
无符号整数算术始终进行 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
二元运算符 / 将第一操作数除以第二操作数(经一般算术转换后)。
对于整型操作数,它产生代数商,
以实现定义的方向对商进行舍入。 |
(C++11 前) |
商向零取整(舍弃小数部分)。 |
(C++11 起) |
若第二操作数为零,则行为未定义,但若进行浮点除法且类型支持 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 为未定义)。
注意:C++11 之前,若二元运算符 % 的一或两个操作数为负,则余数符号是实现定义的,因为它取决于整数除法的舍入方向。函数 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^ 的结果是两个操作数值(经一般算术转换后)的逐位异或。
在针对用户定义运算符的重载决议中,对于每对提升后的整型类型 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 << "\u3000\u3000掩码: " << mask << std::setw(39) << bin16(mask) << '\n' << "\u3000\u3000\u3000值: " << 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'; }
输出:
Mask: 0xf0 0000000011110000 Value: 0x12345678 00010010001101000101011001111000 Setting bits: 0x123456f8 00010010001101000101011011111000 Clearing bits: 0x12345608 00010010001101000101011000001000 Selecting bits: 0x70 00000000000000000000000001110000 XOR-ing bits: 0x12345688 00010010001101000101011010001000 Inverting bits: 0xedcba987 11101101110010111010100110000111
移位运算符
移位运算符表达式的形式为
左操作数 << 右操作数
|
(1) | ||||||||
左操作数 >> 右操作数
|
(2) | ||||||||
返回类型是提升后的左操作数的类型。
对于无符号 对于有符号的非负 对于负 对于无符号 对于负 |
(C++20 前) |
|
(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> 的公开成员函数)
|
流插入/抽取运算符
整个标准库中,移位运算符常对 I/O 流( std::ios_base& 或从它派生的类之一)重载,以之为左运算数和返回类型。这种运算符被称为流插入和流抽取运算符:
提取带格式数据 ( std::basic_istream<CharT,Traits> 的公开成员函数) | |
提取字符和字符数组 (函数模板) | |
插入带格式数据 ( std::basic_ostream<CharT,Traits> 的公开成员函数) | |
插入字符数据 (函数) | |
复数的序列化和反序列化 (函数模板) | |
执行 bitset 的流输入和输出 (函数) | |
执行字符串的流输入与输出 (函数模板) | |
(C++11) |
执行伪随机数引擎的流输入和输出 (函数) |
(C++11) |
执行伪随机数分布的流输入和输出 (函数模板) |
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
DR | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 1457 | C++98 | 将正的有符号值的最左端的 1 位移入符号位为未定义行为
|
使之为良定义 |
参阅
常见运算符 | ||||||
---|---|---|---|---|---|---|
赋值 | 自增 自减 |
算术 | 逻辑 | 比较 | 成员访问 | 其他 |
a = b |
++a |
+a |
!a |
a == b |
a[b] |
a(...) |
特殊运算符 | ||||||
static_cast 转换一个类型为另一相关类型 |