运算符重载

来自cppreference.com
< cpp‎ | language
 
 
 
表达式
概述
值类别(左值 lvalue 、右值 rvalue 、亡值 xvalue )
求值顺序(序列点)
常量表达式
不求值表达式
初等表达式
lambda 表达式(C++11)
字面量
整数字面量
浮点字面量
布尔字面量
字符字面量,包含转义序列
字符串字面量
空指针字面量(C++11)
用户定义字面量(C++11)
运算符
赋值运算符a=b, a+=b, a-=b, a*=b, a/=b, a%=b, a&=b, a|=b, a^=b, a<<=b, a>>=b
自增与自减++a, --a, a++, a--
算术运算符+a, -a, a+b, a-b, a*b, a/b, a%b, ~a, a&b, a|b, a^b, a<<b, a>>b
逻辑运算符a||b, a&&b, !a
比较运算符a==b, a!=b, a<b, a>b, a<=b, a>=b, a<=>b(C++20)
成员访问运算符a[b], *a, &a, a->b, a.b, a->*b, a.*b
其他运算符a(...), a,b, a?b:c
运算符的替代表示
优先级和结合性
折叠表达式(C++17)
new 表达式
delete 表达式
throw 表达式
alignof
sizeof
sizeof...(C++11)
typeid
noexcept(C++11)
运算符重载
类型转换
隐式转换
const_cast
static_cast
reinterpret_cast
dynamic_cast
显式转换 (T)a, T(a)
用户定义转换
 

为用户定义类型的运算数定制 C++ 运算符。

目录

[编辑] 语法

重载的运算符是有特殊函数名的函数

operator op (1)
operator type (2)
operator new
operator new []
(3)
operator delete
operator delete []
(4)
operator "" suffix-identifier (5) (C++11 起)
op - 下列 38 个运算符之一: + - * / % ˆ & | ~ ! = < > += -= *= /= %= ˆ= &= |= << >> >>= <<= == != <= >= && || ++ -- , ->* -> ( ) [ ]
1) 重载的运算符;

[编辑] 重载的运算符

运算符出现于表达式中,且其至少一个运算数拥有类类型枚举类型时,使用重载决议确定要调用的用户定义函数,其签名匹配下列者:

表达式 作为成员函数 作为非成员函数 示例
@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)

此表中, @ 是表示所有匹配运算符的占位符: @a 中为所有前缀运算符, a@ 为异于 -> 的所有后缀运算符, a@b 中为异于 = 的所有其他运算符

注意:对于重载用户定义转换函数用户定义字面量分配解分配,可分别见其专题。

重载的运算符(但非内建运算符)能用函数记法调用:

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::vectorstd::string ),有一常用的便捷方式:复制并交换赋值运算符,它以值接收其参数(从而依赖参数的值类别,工作方式同复制和移动赋值),与形参交换,并令析构函数清理。

T& T::operator=(T arg) noexcept // 调用复制/移动构造函数以构造参数
{
    swap(arg); // 在 *this 与 arg 间交换资源
    return *this;
} // 调用 arg 的析构函数以释放先前 *this 所保有的资源

此形式自动提供强异常保证,但禁止资源复用。

[编辑] 流释出与插入

operator>>operator<< 接收 std::istream&std::ostream& 作为左侧参数的重载,被称为插入与释出运算符。因为它们接收用户定义类型为右参数( a@b 中的 b ),它们必须实现为非成员。

std::ostream& operator<<(std::ostream& os, const T& obj)
{
    // 写对象到流
    return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
    // 从流读对象
    if( /* 不能构造 T */ )
        is.setstate(std::ios::failbit);
    return is;
}

这些运算符有时实现为友元函数

[编辑] 函数调用运算符

用户定义类重载函数调用运算符时 operator() 时,类成为函数对象 (FunctionObject) 类型。许多标准算法,从 std::sortstd::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::memcmpstd::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[] 定义二个重载: const 与 non-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 平面的引用,而它拥有自己返回到 1D 行引用的 operator[] ,行必须有返回到元素引用的 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 的取反。

[编辑] 罕有重载的运算符

下列运算符罕有重载:

  • 取址运算符, 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 库使用 operator,boost.assignboost.spirit 及其他库。数据库访问库 SOCI 亦重载 operator,
  • 通过指向成员指针的成员访问 operator->* 。重载此运算符无特别缺点,但实践中少有使用。有人推荐这能作为智能指针接口的一部分,且实际上在 boost.phoenix 中的 actor 有实际用途。它在 EDSL ,例如 cpp.react 中更常见。

[编辑] 示例

#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 += b
a -= b
a *= b
a /= b
a %= b
a &= b
a |= b
a ^= b
a <<= b
a >>= b

++a
--a
a++
a--

+a
-a
a + b
a - b
a * b
a / b
a % b
~a
a & b
a | b
a ^ b
a << b
a >> b

!a
a && b
a || b

a == b
a != b
a < b
a > b
a <= b
a >= b
a <=> b

a[b]
*a
&a
a->b
a.b
a->*b
a.*b

a(...)
a, b
? :

特殊运算符

static_cast 转换一个类型为另一相关类型
dynamic_cast 在继承层级中转换
const_cast 添加或移除 cv 限定符
reinterpret_cast 转换类型到无关类型
C 风格转型static_castconst_castreinterpret_cast 的混合转换一个类型到另一类型
new 创建有动态存储期的对象
delete 销毁先前由 new 表达式创建的对象,并释放其所拥有的内存区域
sizeof 查询类型的大小
sizeof... 查询形参包的大小(C++11 起)
typeid 查询类型的类型信息
noexcept 查询表达式是否能抛出异常(C++11 起)
alignof 查询类型的对齐要求(C++11 起)

[编辑] 引用

  1. StackOverflow C++ FAQ 上的运算符重载