把 +、-、*、/、==、[]、() 这些原本用于 int、double 等内置类型的运算符,为自定义类再规定一套行为,使运算符可以作用于该类的对象。本质仍是函数,只是函数名写成 operator+ 这种形式;并不是发明新符号,只是重新解释已有运算符。不重载时只能写 p1.add(p2);重载后可写 p1 + p2。不可改变优先级与结合性。本节先给出 Point 的加法,再说明成员与友元两种写法,以及 <<、+= 与下标,并联系函数重载与类。
最简单的例子
写一个 Point 类,希望直接用 p1 + p2 把坐标相加,而不写 p1.add(p2):
#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,6p1 + p2 会被编译器翻译成 p1.operator+(p2)。
两种写法
- 成员函数重载:
operator+写在类里面,左操作数必须是本类对象,即上面的p1 + p2。 - 友元或非成员重载:适合
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,须把该函数声明为友元才能访问。
#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。== 比较两个对象,同样常做成成员:
#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
1C++20 起可定义 <=> 一次生成多种比较(进阶);在此之前须分别实现所需的比较运算符。
下标 []
buf[i] 对应成员 operator[]。下面用固定三个元素演示读写;真正的容器还要保存长度并考虑越界(生产代码可另提供 at() 或断言)。二维下标常用 operator() 写成 m(i, j),规则相同,只是参数变成两个。
#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 声明。