C++ 参考手册
- C++11
- C++14
- C++17
- C++20
- C++ 编译器支持情况表
- 独立与宿主实现
- C++ 语言
- 变量模板(C++14 起)
- 整数字面量
- 聚合初始化
- 比较运算符
- 默认比较(C++20 起)
- 转义序列
- for 循环
- while 循环
- 用户定义转换
- SFINAE
- 主函数
- ASCII 码表
- 标识符
- 类型
- 内存模型
- 对象
- 基本概念
- 表达式
- 声明
- 初始化
- 函数
- 语句
- 类
- 运算符重载
- 模板
- 异常
- 事务性内存
- 占位符类型说明符 (C++11 起)
- decltype 说明符
- 函数声明
- final 说明符 (C++11 起)
- override 说明符(C++11 起)
- 引用声明
- 移动构造函数
- 移动赋值运算符
- 枚举声明
- constexpr 说明符(C++11 起)
- 列表初始化 (C++11 起)
- 构造函数与成员初始化器列表
- using 声明
- nullptr,指针字面量
- 基础类型
- 类型别名,别名模版 (C++11 起)
- 形参包
- 联合体声明
- 字符串字面量
- 用户定义字面量 (C++11 起)
- 属性说明符序列(C++11 起)
- Lambda 表达式 (C++11 起)
- noexcept 说明符 (C++11 起)
- noexcept 运算符 (C++11 起)
- alignof 运算符(C++11 起)
- alignas 说明符 (C++11 起)
- 存储类说明符
- 基于范围的 for 循环 (C++11 起)
- static_assert 声明
- 隐式转换
- 代用运算符表示
- 自增/自减运算符
- 折叠表达式(C++17 起)
- 类模板实参推导(C++17 起)
- 模板形参与模板实参
- if 语句
- inline 说明符
- 结构化绑定声明 (C++17 起)
- switch 语句
- 字符字面量
- 命名空间
- 求值顺序
- 复制消除
- consteval 说明符 (C++20 起)
- constinit 说明符 (C++20 起)
- 协程 (C++20)
- 模块 (C++20 起)
- 约束与概念 (C++20 起)
- new 表达式
- do-while 循环
- continue 语句
- break 语句
- goto 语句
- return 语句
- 动态异常说明
- throw 表达式
- try 块
- 命名空间别名
- 类声明
- cv(const 与 volatile)类型限定符
- 默认初始化
- 值初始化(C++03 起)
- 零初始化
- 复制初始化
- 直接初始化
- 常量初始化
- 引用初始化
- 值类别
- C++ 运算符优先级
- 布尔字面量
- 浮点字面量
- typedef 说明符
- 显式类型转换
- static_cast 转换
- dynamic_cast 转换
- const_cast 转换
- reinterpret_cast 转换
- delete 表达式
- 构造函数与成员初始化器列表
- this 指针
- 访问说明符
- 友元声明
- virtual 函数说明符
- explicit 说明符
- 静态成员
- 默认构造函数
- 复制构造函数
- 复制赋值运算符
- 析构函数
- 类模板
- 函数模板
- 显式(全)模板特化
- 汇编声明
- C++ 的历史
- 作用域
- 生存期
- 定义与单一定义规则(ODR)
- 名字查找
- 有限定的名字查找
- 无限定的名字查找
- 如同规则
- 未定义行为
- 翻译阶段
- 常量表达式
- 赋值运算符
- 算术运算符
- 逻辑运算符
- 成员访问运算符
- 其他运算符
- sizeof 运算符
- typeid 运算符
- 指针声明
- 数组声明
- 语言链接
- 详述类型说明符
- 默认实参
- 变长实参
- 实参依赖查找
- 重载决议
- 重载函数的地址
- 注入类名
- 非静态数据成员
- 非静态成员函数
- 嵌套类
- 派生类
- 空基类优化
- 抽象类
- 位域
- 转换构造函数
- 成员模板
- 模板实参推导
- 部分模板特化
- sizeof... 运算符
- 待决名
- 函数 try 块
- 扩充命名空间 std
- 字母缩写
- RAII
- 三/五/零之法则
- PImpl
- 零开销原则
- 类型
- 隐式转换
- 注释
- C++ 关键词
- 预处理器
- C++ 标准库头文件
- 具名要求
- 功能特性测试 (C++20)
- 工具库
- 类型支持(基本类型、RTTI、类型特性)
- 概念库 (C++20)
- 错误处理
- 动态内存管理
- 日期和时间工具
- 字符串库
- 容器库
- 迭代器库
- 范围库 (C++20)
- 算法库
- 数值库
- 输入/输出库
- 文件系统库
- 本地化库
- 正则表达式库
- 原子操作库
- 线程支持库
- 实验性 C++ 特性
- 有用的资源
- 索引
- std 符号索引
- 协程支持 (C++20)
- C++ 关键词
运算符重载
为用户定义类型的操作数定制 C++ 运算符。
语法
重载的运算符是具有特殊的函数名的函数:
operator op
|
(1) | ||||||||
operator 类型
|
(2) | ||||||||
operator new operator new []
|
(3) | ||||||||
operator delete operator delete []
|
(4) | ||||||||
operator "" 后缀标识符
|
(5) | (C++11 起) | |||||||
operator co_await
|
(6) | (C++20 起) | |||||||
op | - | 下列 38 (C++20 前)39 (C++20 起) 个运算符之一: + - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= <=> (C++20 起) && || ++ -- , ->* -> ( ) [ ] |
重载的运算符
当表达式中出现某个运算符,且其至少一个操作数拥有类类型或枚举类型时,使用重载决议在具有与以下各项匹配的签名的函数中,确定所要调用的用户定义函数:
表达式 | 作为成员函数 | 作为非成员函数 | 示例 |
---|---|---|---|
@a | (a).operator@ ( ) | operator@ (a) | !std::cin 调用 std::cin.operator!() |
a@b | (a).operator@ (b) | operator@ (a, b) | std::cout << 42 调用 std::cout.operator<<(42) |
a=b | (a).operator= (b) | 不能是非成员 | std::string s; s = "abc"; 调用 s.operator=("abc") |
a(b...) | (a).operator()(b...) | 不能是非成员 | std::random_device r; auto n = r(); 调用 r.operator()() |
a[b] | (a).operator[](b) | 不能是非成员 | std::map<int, int> m; m[1] = 2; 调用 m.operator[](1) |
a-> | (a).operator-> ( ) | 不能是非成员 | auto p = std::make_unique<S>(); p->bar() 调用 p.operator->() |
a@ | (a).operator@ (0) | operator@ (a, 0) | std::vector<int>::iterator i = v.begin(); i++ 调用 i.operator++(0) |
此表中, |
另外,对于比较运算符 ==, !=, <, >, <=, >=, <=> ,重载决议亦考虑从 operator== 或 operator<=> 生成的重写候选。 |
(C++20 起) |
注意:对于重载的 co_await
、 (C++20 起)用户定义转换函数、用户定义字面量、分配与解分配,可分别见其专题。
重载的运算符(但非内建运算符)可用函数记法进行调用:
std::string str = "Hello, "; str.operator+=("world"); // 同 str += "world"; operator<<(operator<<(std::cout, str) , '\n'); // 同 std::cout << str << '\n'; // (C++17 起) 但定序不同
限制
- 不能重载运算符
::
(作用域解析)、.
(成员访问)、.*
(通过成员指针的成员访问)及?:
(三元条件)。 - 不能创建新运算符,例如
**
、<>
或&|
。 - 运算符
&&
与||
的重载失去短路求值。 - 重载的运算符
->
必须要么返回裸指针,要么(按引用或值)返回同样重载了运算符->
的对象。 - 不可能更改运算符的优先级、结合方向或操作数的数量。
|
(C++17 前) |
规范实现
除了上述限制外,语言对重载运算符的所作所为或返回类型(它不参与重载决议)上没有其他任何制约,但通常期待重载的运算符表现尽可能与内建运算符相似:期待 operator+ 对其实参进行相加而非相乘,期待 operator= 进行赋值,如此等等。期待相关的运算符之间的表现也相似(operator+ 与 operator+= 做同一类加法运算)。返回类型为期待使用该运算符的表达式所限制:例如,令赋值运算符按引用返回,以使写出 a = b = c = d 可行,因为内建运算符允许这样做。
常见的重载运算符拥有下列典型、规范形式:[1]
赋值运算符
赋值运算符(operator=)有特殊性质:细节见复制赋值与移动赋值。
对规范的复制赋值运算符,期待其在自赋值时不进行操作,并按引用返回 lhs:
// 假设对象保有可重用存储,例如一个分配于堆的缓冲区 mArray T& operator=(const T& other) // 复制赋值 { if (this != &other) { // 期待检查自赋值 if (other.size != size) { // 存储不可复用 delete[] mArray; // 销毁 this 中的存储 size = 0; mArray = nullptr; // 在下一行抛出的情况下维持不变式 mArray = new int[other.size]; // 分配 this 中的存储 size = other.size; } std::copy(other.mArray, other.mArray + other.size, mArray); } return *this; }
对规范的移动赋值,期待其令被移动对象遗留于合法状态(即有完好类不变式的状态),且在自赋值时要么不做任何事,要么至少遗留对象于合法状态,并以非 const 引用返回 lhs,而且为 noexcept:
T& operator=(T&& other) noexcept // 移动赋值 { if(this != &other) { // 自赋值时无操作(delete[]/size=0 亦 OK) delete[] mArray; // 删除此存储 mArray = std::exchange(other.mArray, nullptr); // 令被移动对象遗留于合法状态 size = std::exchange(other.size, 0); } return *this; }
在复制赋值不能从资源复用中受益的情形下(它不管理堆分配数组,且无这么做的(可能传递的)成员,例如 std::vector 或 std::string 成员),有一种流行的便捷方式:复制并交换(copy-and-swap)赋值运算符,它按值接收形参(从而根据实参的值类别而同时支持复制和移动赋值),交换形参,并令析构函数进行清理。
这种形式自动提供强异常保证,但禁止资源复用。
流的提取与插入
接受 std::istream& 或 std::ostream& 作为左侧实参的 operator>>
与 operator<<
的重载,被称为插入与提取运算符。因为它们接收用户定义类型为右实参(a@b 中的 b
),所以它们必须实现为非成员。
std::ostream& operator<<(std::ostream& os, const T& obj) { // 向流写入 obj return os; } std::istream& operator>>(std::istream& is, T& obj) { // 从流读取 obj if( /* 不能构造 T */ ) is.setstate(std::ios::failbit); return is; }
这些运算符有时实现为友元函数。
函数调用运算符
当用户定义的类重载了函数调用运算符 operator() 时,它就成为函数对象 (FunctionObject) 类型。从 std::sort 到 std::accumulate 的许多标准算法都接受这种类型的对象以定制其行为。operator() 没有特别值得注意的规范形式,此处演示其用法
struct Sum { int sum; Sum() : sum(0) { } void operator()(int n) { sum += n; } }; Sum s = std::for_each(v.begin(), v.end(), Sum());
自增与自减
当表达式中出现后缀自增与自减时,以一个整数实参 0
调用用户定义函数(operator++ 或 operator--)。它典型地实现为 T operator++(int),其中参数被忽略。后缀自增与自减运算符通常以前缀版本实现:
struct X { X& operator++() { // actual increment takes place here return *this; } X operator++(int) { X tmp(*this); // copy operator++(); // pre-increment return tmp; // return old value } };
尽管前自增/前自减的规范形式是返回引用的,但同任何运算符重载一样,其返回类型是用户定义的;例如这些运算符对 std::atomic 的重载以值返回。
二元算术运算符
典型情况下,二元运算符都被实现为非成员以维持对称性(例如,将复数与整数相加时,若 operator+
是复数类型的成员函数,则唯有 complex+integer 能编译,而 integer+complex 不能)。因为对于每个二元算术运算符都存在对应的复合赋值运算符,所以二元运算符的规范形式是基于其对应的复合赋值实现的:
class X { public: X& operator+=(const X& rhs) // 复合赋值(不必,但通常是成员函数,以修改私有成员) { /* 将 rhs 加到 *this 发生于此 */ return *this; // 按引用返回结果 } // 定义于类体内的友元是 inline 的,且在非 ADL 查找中被隐藏 friend X operator+(X lhs, // 按值传递 lhs 有助于优化链状的 a+b+c const X& rhs) // 否则,两个形参都是 const 引用 { lhs += rhs; // 复用复合赋值 return lhs; // 按值返回结果(使用移动构造函数) } };
关系运算符
标准的算法(如 std::sort)和容器(如 std::set),默认情况下期待 operator< 对于用户提供的类型有定义,并期待它实现严格弱序(从而满足比较 (Compare) 要求)。一种为结构体实现严格弱序的惯用方式是使用 std::tie 提供的字典序比较:
struct Record { std::string name; unsigned int floor; double weight; friend bool operator<(const Record& l, const Record& r) { return std::tie(l.name, l.floor, l.weight) < std::tie(r.name, r.floor, r.weight); // 保持相同顺序 } };
典型地,一旦提供了 operator<,其他关系运算符就都能通过 operator< 来实现。
inline bool operator< (const X& lhs, const X& rhs){ /* 做实际比较 */ } inline bool operator> (const X& lhs, const X& rhs){ return rhs < lhs; } inline bool operator<=(const X& lhs, const X& rhs){ return !(lhs > rhs); } inline bool operator>=(const X& lhs, const X& rhs){ return !(lhs < rhs); }
类似地,不相等运算符典型地通过 operator== 来实现:
inline bool operator==(const X& lhs, const X& rhs){ /* 做实际比较 */ } inline bool operator!=(const X& lhs, const X& rhs){ return !(lhs == rhs); }
当提供了三路比较(如 std::memcmp 或 std::string::compare)时,所有六个关系运算符都能通过它表达:
inline bool operator==(const X& lhs, const X& rhs){ return cmp(lhs,rhs) == 0; } inline bool operator!=(const X& lhs, const X& rhs){ return cmp(lhs,rhs) != 0; } inline bool operator< (const X& lhs, const X& rhs){ return cmp(lhs,rhs) < 0; } inline bool operator> (const X& lhs, const X& rhs){ return cmp(lhs,rhs) > 0; } inline bool operator<=(const X& lhs, const X& rhs){ return cmp(lhs,rhs) <= 0; } inline bool operator>=(const X& lhs, const X& rhs){ return cmp(lhs,rhs) >= 0; }
若定义 operator== 则编译器会自动生成不等运算符。类似地,若定义三路比较运算符 operator<=> 则编译器会自动生成四个关系运算符。若定义 operator<=> 为预置,则编译器会生成 operator== 与 operator<=> : struct Record { std::string name; unsigned int floor; double weight; auto operator<=>(const Record&) = default; }; // 现在能用 ==、!=、<、<=、> 和 >= 比较 Record 细节见默认比较。 |
(C++20 起) |
数组下标运算符
提供数组式访问并同时允许读写的用户定义类,典型地为 operator[] 定义两个重载:const 和非 const 变体:
struct T { value_t& operator[](std::size_t idx) { return mVector[idx]; } const value_t& operator[](std::size_t idx) const { return mVector[idx]; } };
若已知值类型是内建类型,则 const 变体应当按值返回。
当不希望或不可能直接访问容器元素,或者要区别左值(c[i] = v;)和右值(v = c[i];)的不同用法时,operator[] 可以返回一个代理。示例见 std::bitset::operator[]。
为提供多维数组访问语义,例如实现 3D 数组访问 a[i][j][k] = x;,operator[] 必须返回到 2D 平面的引用,它必须拥有自己的 operator[] 并返回到 1D 行的引用,而行必须拥有返回到元素的引用的 operator[]。为避免这种复杂性,一些库选择代之以重载 operator(),使得 3D 访问表达式拥有 Fortran 式的语法 a(i, j, k) = x;。
逐位算术运算符
实现位掩码类型 (BitmaskType) 的规定的用户定义类和枚举,要求重载逐位算术运算符 operator&、operator|、operator^、operator~、operator&=、operator|= 及 operator^=,而且可选地重载位移运算符 operator<<、operator>>、operator>>= 及 operator<<=。规范实现通常遵循上述的二元算术运算符。
布尔取反运算符
运算符 operator! 常为有意用于布尔语境的用户定义类所重载。这种类亦提供用户定义转换函数 explicit operator bool()(标准库样例见 std::basic_ios),而 operator! 的受期待行为是返回 operator bool 的取反。 |
(C++11 前) |
由于内建运算符 ! 进行按语境到 |
(C++11 起) |
罕有重载的运算符
下列运算符罕有重载:
- 取址运算符 operator&。如果对不完整类型的左值应用一元 &,而完整类型声明了重载的 operator&,则行为未定义 (C++11 前)运算符拥有内建含义还是调用运算符函数是未指明的 (C++11 起)。因为此运算符可能被重载,所以泛型库都用 std::addressof 取得用户定义类型的对象的地址。最为人熟知的规范重载的 operator& 是 Microsoft 类 CComPtr。在 boost.spirit 中可以找到它在 EDSL 的使用案例。
- 布尔逻辑运算符 operator&& 与 operator||。不同于内建版本,重载版本无法实现短路求值。而且不同于内建版本,它们也不会令左操作数的求值按顺序早于右操作数。 (C++17 前)标准库中,这些运算符仅为 std::valarray 重载。
- 逗号运算符 operator,。不同于内建版本,重载版本不会令左操作数的求值按顺序早于右操作数。 (C++17 前)因为此运算符可能被重载,所以泛型库都用 a,void(),b 这种表达式取代 a,b,以对用户定义类型的表达式按顺序求值。boost 库在 boost.assign、boost.spirit 及几个其他库中使用
operator,
。数据库访问库 SOCI 亦重载operator,
。 - 通过成员指针的成员访问 operator->*。重载此运算符无特别缺点,但实践中少有使用。有人推荐这能作为智能指针接口的一部分,且实际上在 boost.phoenix 中的 actor 有实际用途。它在如 cpp.react 这样的 EDSL 中更常见。
示例
#include <iostream> class Fraction { int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } int n, d; public: Fraction(int n, int d = 1) : n(n/gcd(n, d)), d(d/gcd(n, d)) { } int num() const { return n; } int den() const { return d; } Fraction& operator*=(const Fraction& rhs) { int new_n = n * rhs.n/gcd(n * rhs.n, d * rhs.d); d = d * rhs.d/gcd(n * rhs.n, d * rhs.d); n = new_n; return *this; } }; std::ostream& operator<<(std::ostream& out, const Fraction& f) { return out << f.num() << '/' << f.den() ; } bool operator==(const Fraction& lhs, const Fraction& rhs) { return lhs.num() == rhs.num() && lhs.den() == rhs.den(); } bool operator!=(const Fraction& lhs, const Fraction& rhs) { return !(lhs == rhs); } Fraction operator*(Fraction lhs, const Fraction& rhs) { return lhs *= rhs; } int main() { Fraction f1(3, 8), f2(1, 2), f3(10, 2); std::cout << f1 << " * " << f2 << " = " << f1 * f2 << '\n' << f2 << " * " << f3 << " = " << f2 * f3 << '\n' << 2 << " * " << f1 << " = " << 2 * f1 << '\n'; }
输出:
3/8 * 1/2 = 3/16 1/2 * 5/1 = 5/2 2 * 3/8 = 3/4
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
DR | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 1458 | C++11 | 对重载了取址运算符的不完整类型取地址是未定义行为 | 行为只是未指明 |
参阅
常见运算符 | ||||||
---|---|---|---|---|---|---|
赋值 | 自增 自减 |
算术 | 逻辑 | 比较 | 成员访问 | 其他 |
a = b |
++a |
+a |
!a |
a == b |
a[b] |
a(...) |
特殊运算符 | ||||||
static_cast 转换一个类型为另一相关类型 |