运算符重载:让C++类型像内置类型一样自然
运算符重载,说白了就是让自定义类型也能用上 +、-、== 这些符号。我刚开始学C++时觉得这功能挺花哨的,直到在项目中写了一个矩阵类——如果不重载 +,每次加法都得写 matrix.add(other),那代码读起来简直要命。
嗯,这里要注意:运算符重载不是让你发明新符号,而是给已有运算符赋予新的含义。C++不允许你创建 ** 这样的运算符,也不允许改变运算符的优先级和结合性。
核心原则:运算符重载应该让代码更自然,而不是更花哨。如果你重载了 +,别人看到 a + b 时应该能直观理解它的含义。
重载规则:哪些能重载,哪些不能
C++里大部分运算符都能重载,但有几个例外。我记得刚入行时有个同事试图重载 ::(作用域解析),编译器直接报错——有些东西是语言底层的规则,不能碰。
可以重载的运算符(大部分):
- 算术运算符:
+-*/% - 关系运算符:
==!=<><=>= - 赋值运算符:
= - 下标运算符:
[] - 函数调用运算符:
() - 输入输出运算符:
<<>> - 自增自减:
++-- - 等等
不能重载的运算符(记住这5个):
| 运算符 | 原因 |
|---|---|
:: | 作用域解析,语言核心机制 |
. | 成员访问,不能改变 |
.* | 成员指针访问 |
? : | 三目运算符 |
sizeof | 类型大小查询 |
个人习惯:我一般把重载函数写在类内部(作为成员函数),但像 << 和 >> 这种需要操作左值的,我会写成友元函数。为什么?因为 cout << obj 的左侧是 ostream,不是你的类对象。
重载算术运算符:让对象能算数
假设我们有一个 Vector2D 类,表示二维向量。你想想看,如果每次加法都要写 v1.add(v2),那代码得多啰嗦?
class Vector2D {
public:
double x, y;
Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// 重载 + 运算符
Vector2D operator+(const Vector2D& other) const {
return Vector2D(x + other.x, y + other.y);
}
// 重载 += 运算符
Vector2D& operator+=(const Vector2D& other) {
x += other.x;
y += other.y;
return *this;
}
// 重载 * 运算符(标量乘法)
Vector2D operator*(double scalar) const {
return Vector2D(x * scalar, y * scalar);
}
};
// 使用示例
Vector2D v1(1, 2), v2(3, 4);
Vector2D v3 = v1 + v2; // 多自然!
v1 += v2;
Vector2D v4 = v1 * 2.5;
我在项目中遇到过一个问题:重载 + 时忘了加 const,结果 v1 + v2 之后 v1 的值被改了。嗯,这是个经典坑——operator+ 应该返回新对象,不修改自身。所以一定要加 const 修饰。
我曾经踩过的坑:重载 + 时返回了引用,结果返回的是局部变量的引用,程序直接崩溃。记住:operator+ 返回新对象(值返回),operator+= 返回自身引用。
重载关系运算符:让对象能比较
关系运算符重载,最常用的就是 == 和 <。为什么?因为很多标准库容器(比如 std::map、std::set)需要你的类型支持比较。
class Person {
public:
std::string name;
int age;
Person(const std::string& name, int age)
: name(name), age(age) {}
// 重载 ==
bool operator==(const Person& other) const {
return name == other.name && age == other.age;
}
// 重载 != (通常基于 == 实现)
bool operator!=(const Person& other) const {
return !(*this == other);
}
// 重载 < (用于排序)
bool operator<(const Person& other) const {
if (name != other.name) return name < other.name;
return age < other.age;
}
};
我个人习惯:只实现 == 和 <,然后用它们推导出其他关系运算符。比如 != 就是 !(a == b),> 就是 b < a。这样代码量少,也不容易出错。
小技巧:C++20 引入了三路比较运算符 <=>(飞船运算符),你只需要实现这一个,编译器就能自动生成所有关系运算符。可惜很多老项目还在用 C++17,所以手写 == 和 < 还是基本功。
重载输入输出运算符:让对象能打印
调试代码时最烦什么?不能直接 cout << myObject。重载 << 和 >> 就能解决这个问题。
class Point {
public:
int x, y;
Point(int x = 0, int y = 0) : x(x), y(y) {}
// 友元声明,让非成员函数能访问私有成员
friend std::ostream& operator<<(std::ostream& os, const Point& p);
friend std::istream& operator>>(std::istream& is, Point& p);
};
// 输出运算符重载
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x << ", " << p.y << ")";
return os;
}
// 输入运算符重载
std::istream& operator>>(std::istream& is, Point& p) {
is >> p.x >> p.y;
return is;
}
// 使用
Point p(3, 4);
std::cout << "Point: " << p << std::endl; // 输出: Point: (3, 4)
为什么 << 和 >> 要写成非成员函数?因为左侧操作数是 ostream 或 istream,不是你的类对象。你没法在 ostream 类里加成员函数,所以只能写成全局函数,再用 friend 访问私有成员。
我曾经犯过的错:重载 << 时忘了返回 os 引用,结果链式调用 cout << a << b 直接编译失败。记住:一定要 return os;,这样才能连续输出。
函数调用运算符与仿函数:让对象像函数一样用
这个特性很有意思。你想想看,一个对象居然能像函数一样被调用——只要重载 () 运算符就行。这种对象叫「仿函数」或「函数对象」。
class Adder {
public:
int operator()(int a, int b) const {
return a + b;
}
};
// 使用
Adder add;
int result = add(3, 4); // 像函数一样调用!
std::cout << result << std::endl; // 输出 7
仿函数最大的优势是什么?它可以保存状态。普通函数做不到这一点,但仿函数可以。
class Counter {
private:
int count = 0;
public:
int operator()() {
return ++count;
}
int getCount() const { return count; }
};
Counter counter;
std::cout << counter() << std::endl; // 1
std::cout << counter() << std::endl; // 2
std::cout << counter() << std::endl; // 3
我在项目中用仿函数做过一个过滤器:根据不同的条件筛选数据。把筛选条件封装成仿函数,传给 std::find_if 或 std::remove_if,代码特别干净。
class GreaterThan {
private:
int threshold;
public:
GreaterThan(int t) : threshold(t) {}
bool operator()(int value) const {
return value > threshold;
}
};
std::vector<int> data = {1, 5, 3, 8, 2, 9};
// 找出所有大于5的元素
auto it = std::find_if(data.begin(), data.end(), GreaterThan(5));
// 或者用 lambda(C++11 之后更常用)
auto it2 = std::find_if(data.begin(), data.end(),
[](int x) { return x > 5; });
个人建议:C++11 之后有了 lambda 表达式,很多场景下比仿函数更简洁。但仿函数依然有它的价值——当你需要复用同一个带状态的函数对象时,仿函数更清晰。比如在算法竞赛或性能敏感场景,仿函数比 lambda 更容易内联优化。
知识体系总览
下面这张图总结了运算符重载的核心知识点,方便你快速回顾:
这张图把运算符重载分成了五大块:规则、算术、关系、IO、仿函数。每一块都有它的应用场景和注意事项。我个人觉得,掌握这五块内容,日常开发中90%的运算符重载需求都能搞定。
总结一下:运算符重载不是炫技,而是为了让代码更自然、更易读。重载时记住三点——保持语义一致、注意 const 正确性、能用非成员函数就别用成员函数(尤其是 IO 运算符)。做到这三点,你的代码既专业又优雅。