16、运算符重载:重载规则、常见运算符重载(+ - * / == << >>)、函数调用运算符(仿函数)、类型转换运算符
运算符重载,说白了就是让 C++ 的运算符能“认识”你自定义的类型。我刚开始学的时候觉得这玩意儿挺花哨,后来在项目里写一个矩阵类,发现如果没有重载 + 和 *,代码会变成一坨屎——到处都是 matrix.add(matrix2) 这种调用,可读性极差。嗯,从那以后我就明白了,运算符重载不是为了炫技,而是为了让代码更自然。
16.1 重载规则:别踩红线
运算符重载不是你想怎么来就怎么来的。C++ 有一整套规则,我帮你梳理一下最核心的几条。
- 不能发明新运算符:你只能重载已有的运算符,比如
+、-、*、/,不能自己造个**出来。 - 不改变优先级和结合性:重载后的
+依然是左结合,优先级不变。我曾经见过有人试图让+比*优先,结果编译器直接报错。 - 不能重载的运算符:
::、.、.*、?:、sizeof、typeid。这些是 C++ 的“禁区”,别去碰。 - 至少有一个操作数是用户定义类型:你不能重载
int + int的行为,那是语言内置的。 - 保持语义一致:重载
+就应该做加法或拼接,别搞成减法。我见过一个项目里有人把+重载成“删除元素”,那代码简直没法维护。
核心原则:运算符重载应该让代码更直观,而不是更混乱。如果你重载后别人看不懂,那还不如写个普通函数。
16.2 常见运算符重载:+ - * / == << >>
这部分是实战中最常用的。我一个个讲,每个都带代码示例。
16.2.1 算术运算符:+ - * /
通常重载为非成员函数,这样支持隐式类型转换。比如 3 + obj 和 obj + 3 都能工作。
class Complex {
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 复合赋值运算符通常重载为成员函数
Complex& operator+=(const Complex& other) {
real += other.real;
imag += other.imag;
return *this;
}
// 友元声明,让非成员函数能访问私有成员
friend Complex operator+(const Complex& lhs, const Complex& rhs);
private:
double real, imag;
};
// 非成员函数实现
Complex operator+(const Complex& lhs, const Complex& rhs) {
Complex temp(lhs);
temp += rhs; // 复用 +=
return temp;
}
我个人习惯把 += 实现成成员函数,然后 + 基于 += 实现。这样代码复用率高,而且 += 返回引用,效率更高。
小技巧:-、*、/ 的模式完全一样。你只需要改一下运算符和内部逻辑,其他结构照搬就行。
16.2.2 比较运算符:==
重载 == 时,记得也要重载 !=。C++20 之后有 operator<=> 可以自动生成,但 C++17 及之前你得手动写。
bool operator==(const Complex& lhs, const Complex& rhs) {
return (lhs.real == rhs.real) && (lhs.imag == rhs.imag);
}
bool operator!=(const Complex& lhs, const Complex& rhs) {
return !(lhs == rhs);
}
我曾经在项目里只重载了 ==,忘了 !=,结果编译时一堆错误。嗯,从那以后我都是成对重载。
16.2.3 输入输出运算符:<< >>
这两个必须重载为非成员函数,因为左操作数是 ostream 或 istream,你不能修改标准库的类。
// 输出运算符
std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << "(" << c.real << ", " << c.imag << "i)";
return os;
}
// 输入运算符
std::istream& operator>>(std::istream& is, Complex& c) {
double r, i;
is >> r >> i;
if (is) { // 检查输入是否成功
c.real = r;
c.imag = i;
}
return is;
}
注意:输入运算符一定要检查输入状态。我曾经没加 if (is),结果用户输入非法字符时,对象被污染了,后续计算全错。血的教训。
16.3 函数调用运算符(仿函数)
重载 operator() 可以让对象像函数一样被调用。这种对象叫“仿函数”或“函数对象”。
class Adder {
public:
int operator()(int a, int b) const {
return a + b;
}
};
// 使用
Adder add;
int result = add(3, 4); // 像函数一样调用
std::cout << result; // 输出 7
仿函数最大的优势是可以携带状态。普通函数做不到这一点。
class ThresholdFilter {
public:
explicit ThresholdFilter(int threshold) : threshold_(threshold) {}
bool operator()(int value) const {
return value > threshold_;
}
private:
int threshold_;
};
// 使用
ThresholdFilter filter(10);
std::vector<int> data = {5, 12, 8, 20};
auto it = std::find_if(data.begin(), data.end(), filter);
// it 指向第一个大于 10 的元素,即 12
我在项目里用仿函数做自定义排序特别多。比如按某个字段排序,把比较逻辑封装在仿函数里,传给 std::sort,代码干净又灵活。
为什么用仿函数而不是 lambda? C++11 之后 lambda 确实更方便,但仿函数在需要多次复用同一逻辑且携带复杂状态时,可读性更好。我个人习惯:简单场景用 lambda,复杂场景用仿函数。
16.4 类型转换运算符
类型转换运算符允许你的类隐式或显式地转换成其他类型。语法是 operator 目标类型()。
class Rational {
public:
Rational(int num = 0, int den = 1) : numerator_(num), denominator_(den) {}
// 隐式转换为 double
operator double() const {
return static_cast<double>(numerator_) / denominator_;
}
// 显式转换为 int(C++11 起)
explicit operator int() const {
return numerator_ / denominator_;
}
private:
int numerator_, denominator_;
};
// 使用
Rational r(3, 4);
double d = r; // 隐式转换,d = 0.75
int i = static_cast<int>(r); // 显式转换,i = 0(整数除法)
小心隐式转换的陷阱:我曾经在项目里写了一个 operator bool(),结果导致 if (obj) 能编译通过,但 obj + 5 也能编译通过(因为 bool 可以隐式转 int)。从那以后,我所有类型转换运算符都加 explicit,除非有非常明确的理由不这么做。
你想想看,类型转换运算符就像一把双刃剑。用得好,代码简洁;用不好,隐式转换会让你 debug 到怀疑人生。我建议:默认加 explicit,需要隐式转换时再去掉。
知识体系图
下面这张图帮你理清运算符重载的整体脉络。我画的时候特意把“规则”放在最上面,因为那是地基,地基不稳什么都白搭。
好了,运算符重载的核心内容就这些。记住规则,多写多练,你很快就能掌握。我在项目里用这些技术写过矩阵库、复数类、甚至一个简单的 DSL 解析器,每次都觉得 C++ 的运算符重载真是把双刃剑——用好了代码优雅,用砸了就是灾难。嗯,希望你成为前者。
公众号:蓝海资料掘金营,微信 deep3321