在C++中,运算符原本只能用于int、double等内置类型,但当我们定义了自己的类,比如表示二维向量或复数的类型,就希望这些对象也能直接用加号、等号等符号来操作。运算符重载正是解决这一问题的语言特性,它允许我们为类定义特定运算符对应的函数,使对象操作更直观自然。

一、运算符重载的基本概念
运算符重载本质上是函数重载的一种特殊形式。当编译器看到对象a和b之间使用某个运算符时,会去寻找对应的operator函数。例如,表达式a + b会被解释为a.operator+(b)(如果是成员函数形式)或operator+(a, b)(如果是非成员函数形式)。
并不是所有运算符都能重载,像作用域解析符::、三元条件运算符?:、成员访问符.等就不能重载。而且重载时不能创造新的运算符符号,也不能改变原有运算符的优先级、结合性和操作数个数。这些限制保证了语言的基本逻辑不被破坏。
1.1 成员函数与非成员函数
以二元加法运算符为例,如果写成类的成员函数,左操作数就是调用对象,右操作数作为函数参数。这种方式简洁,但要求左操作数必须是该类类型。如果希望支持如整数加对象的写法(比如3 + a),则左操作数是int,无法调用成员函数,此时需要定义为非成员函数,通常声明为类的友元以访问私有数据。
下面用一个简单的计数器类展示两种写法差异。成员函数形式隐藏了左操作数,适合对称操作数场景;友元函数形式更灵活,在需要转换左操作数类型时必不可少。选择哪种要看具体使用场景,而不是随意决定。
#include <iostream>
using namespace std;
class Counter {
private:
int value;
public:
Counter(int v = 0) : value(v) {}
int get() const { return value; }
// 成员函数重载加法
Counter operator+(const Counter& other) const {
return Counter(value + other.value);
}
// 友元函数声明
friend Counter operator+(int lhs, const Counter& rhs);
};
// 非成员函数重载,支持 int + Counter
Counter operator+(int lhs, const Counter& rhs) {
return Counter(lhs + rhs.get());
}
int main() {
Counter a(5), b(3);
Counter c = a + b; // 调用成员函数
Counter d = 10 + a; // 调用友元函数
cout << c.get() << " " << d.get() << endl;
return 0;
}
二、常见运算符的重载实践
赋值运算符=是最常被重载的运算符之一。编译器会默认生成浅拷贝版本,但如果类里管理了动态内存,就必须自己实现深拷贝,并且处理自赋值情况。返回类型通常为本类引用,以支持链式赋值如a = b = c。
下标运算符[]也常需要重载,尤其是容器类。它应该返回元素的引用,以便既能读也能写。同时可以提供const版本,供常量对象调用。下面以简易动态数组为例说明。
2.1 赋值与下标运算符示例
在下面的代码中,我们实现了一个IntArray类,重载了赋值运算符和下标运算符。赋值运算符先判断自赋值,再释放旧空间、分配新空间并拷贝数据,最后返回*this。下标运算符区分了普通与const版本,确保常量对象只能读取。
注意重载下标运算符时,参数通常是size_t类型的索引。越界检查可以根据需要决定,在示例中简单使用了assert,实际项目可能抛异常或做更安全的处理。这种重载让对象用起来和原生数组几乎一样。
#include <cassert>
#include <utility>
class IntArray {
int* data;
size_t size;
public:
IntArray(size_t n) : data(new int[n]), size(n) {}
~IntArray() { delete[] data; }
// 重载赋值运算符
IntArray& operator=(const IntArray& other) {
if (this == &other) return *this; // 自赋值保护
delete[] data;
size = other.size;
data = new int[size];
for (size_t i = 0; i < size; ++i) data[i] = other.data[i];
return *this;
}
// 普通版本下标
int& operator[](size_t index) {
assert(index < size);
return data[index];
}
// const版本下标
const int& operator[](size_t index) const {
assert(index < size);
return data[index];
}
};
三、重载时的注意事项与陷阱
重载运算符虽然方便,但容易踩坑。首先,不要返回局部变量的引用。比如重载加法时如果返回引用,而结果是函数内创建的临时对象,函数结束引用就悬空了。加法通常返回新对象,而赋值、复合赋值可返回引用。
其次,重载关系运算符如==、<时,应保持逻辑一致性。如果定义了==,最好也定义!=,并且语义上互补。另外,输入输出运算符<<和>>只能作为非成员函数重载,因为它们左操作数是ostream或istream,不是你的类。
3.1 输入输出运算符重载
下面展示如何重载<<让对象可以直接用cout输出。由于左操作数是ostream,必须写成友元非成员函数。函数内直接访问私有成员打印,返回ostream引用以支持连续输出。
这种重载极大提升了调试和日志输出的效率。配合之前定义的Counter类,就能写cout << a << endl;这样的代码。但要注意,如果类结构复杂,输出格式应稳定,避免给使用者带来困惑。
#include <iostream>
class Point {
int x, y;
public:
Point(int x = 0, int y = 0) : x(x), y(y) {}
friend std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x << ", " << p.y << ")";
return os;
}
};
int main() {
Point p(3, 4);
std::cout << p << std::endl;
return 0;
}
四、总结
通过运算符重载,C++类的对象可以拥有和内置类型一样自然的语法表达。我们既可以用成员函数处理对称操作,也可以用友元函数照顾左操作数转换。赋值、下标、输入输出等运算符各有惯用法与陷阱,写的时候要分清返回值类型和自赋值等问题。
合理重载能增强代码可读性,但也要克制,不要重载得让代码含义模糊。理解编译器如何将符号映射到函数调用,是写好重载的前提。掌握了这些,你就能用C++写出既高效又优雅的对象操作接口。