把 +、-、*、/、==、[]、() 这些原本用于 int、double 等内置类型的运算符,为自定义类再规定一套行为,使运算符可以作用于该类的对象。本质仍是函数,只是函数名写成 operator+ 这种形式;并不是发明新符号,只是重新解释已有运算符。不重载时只能写 p1.add(p2);重载后可写 p1 + p2。不可改变优先级与结合性。本节先给出 Point 的加法,再说明成员与友元两种写法,以及 <<、+= 与下标,并联系函数重载与类。

最简单的例子

写一个 Point 类,希望直接用 p1 + p2 把坐标相加,而不写 p1.add(p2):

C++ 重载点的加法运行
#include <iostream>

class Point {
public:
    int x, y;
    Point(int a = 0, int b = 0) : x(a), y(b) {}

    Point operator+(const Point& other) const {  // 重载 +
        return Point(x + other.x, y + other.y);
    }
};

int main() {
    Point p1(1, 2), p2(3, 4);
    Point res = p1 + p2;  // 等价于 p1.operator+(p2)
    std::cout << res.x << "," << res.y << "\n";
    return 0;
}

输出:

4,6

p1 + p2 会被编译器翻译成 p1.operator+(p2)。

两种写法

  1. 成员函数重载:operator+ 写在类里面,左操作数必须是本类对象,即上面的 p1 + p2。
  2. 友元或非成员重载:适合 cout << obj、3 + p 这种左操作数不是本类对象的情形。字段若是 private,类外函数须声明为友元才能读取:
friend Point operator+(const Point& a, const Point& b) {
    return Point(a.x + b.x, a.y + b.y);
}

成员形式与两个 Point 参数的友元形式,对 p1 + p2 效果相同;差别出现在左操作数不是 Point 时,见下一小节的 <<。

输出运算符 <<

std::cout << c 的左操作数是流,不是 Vec2,所以 operator<< 不能做成 Vec2 的成员,只能写成类外函数。下面字段是 public,函数可以直接读 v.x;若改成 private,须把该函数声明为友元才能访问。

C++ 重载向量输出运行
#include <iostream>

class Vec2 {
public:
    double x = 0;
    double y = 0;
};

std::ostream& operator<<(std::ostream& os, const Vec2& v) {  // 左操作数是 os
    return os << v.x << " " << v.y;
}

int main() {
    Vec2 a{1, 2};
    std::cout << a << "\n";
    return 0;
}

输出:

1 2

非成员运算符与对称性

成员 operator+ 要求左边已经是 Vec2,因此可以表达 v + 2(若右操作数写成 int),但不能表达 2 + v(左边是 int,没有名为 operator+ 的成员)。两边都要写时,把 operator+ 放到类外,两个参数对等:

Vec2 operator+(const Vec2& a, const Vec2& b) {  // 非成员:a + b 即 operator+(a, b)
    return Vec2{a.x + b.x, a.y + b.y};
}

若还要支持 2 + v,再提供 operator+(int, const Vec2&)。

复合赋值与比较

+= 会修改左操作数,因此做成成员并返回 *this,以便写成 c += 1。== 比较两个对象,同样常做成成员:

C++ 重载 += 与 ==运行
#include <iostream>

class Counter {
public:
    int n = 0;
    Counter& operator+=(int d) {
        n += d;
        return *this;  // 返回自身,可继续写 (c += 1) += 2
    }
    bool operator==(const Counter& o) const {
        return n == o.n;
    }
};

int main() {
    Counter c;
    c.n = 1;
    c += 2;
    Counter t;
    t.n = 3;
    std::cout << c.n << "\n";
    std::cout << (c == t) << "\n";
    return 0;
}

输出:

3
1

C++20 起可定义 <=> 一次生成多种比较(进阶);在此之前须分别实现所需的比较运算符。

下标 []

buf[i] 对应成员 operator[]。下面用固定三个元素演示读写;真正的容器还要保存长度并考虑越界(生产代码可另提供 at() 或断言)。二维下标常用 operator() 写成 m(i, j),规则相同,只是参数变成两个。

C++ 重载下标运行
#include <iostream>

class Buffer {
    int data_[3] = {10, 20, 30};
public:
    int& operator[](int i) {  // buf[i] 即 buf.operator[](i)
        return data_[i];
    }
};

int main() {
    Buffer b;
    b[1] = 99;
    std::cout << b[0] << " " << b[1] << " " << b[2] << "\n";
    return 0;
}

输出:

10 99 30

可重载与不可重载(节选)

可重载(常见)不可重载
+ - * / == < << () []:: .* ?: sizeof typeid

设计原则

  • 保持语义:+ 应接近加法或领域里约定的合并,不要让 + 去做删除文件这类无关操作。
  • 与内置一致:不要对 int 等内置类型再提供全局 operator+。
  • 优先复合赋值:先实现 +=,+ 再调用 +=,两处逻辑不容易分叉。
  • IO 运算符:<<、>> 的左操作数是流,宜写成非成员函数。

本节说明了运算符重载的基本形式与注意点。下一节介绍命名空间:组织标识符与 using 声明。