一、前置知识:运算符重载
在 C++ 中,运算符本质上类似于函数。比如加法操作,相当于调用一个函数,左右操作数就是参数。一元运算符只有一个参数,二元运算符有两个。因此,我们可以像重载函数一样重载运算符,让同一个符号根据操作数的不同调用不同的实现,从而支持多态。
不过要注意,语法内置的运算符不能随意重载。例如两个整型相加的规则是确定的,无法更改。但如果操作数是自定义类型(如类),编译器不知道如何运算,这时就需要我们定义规则。比如日期类加上一个整数,表示增加天数,这就是有意义的重载场景。
运算符重载的核心规则
- 转换机制:当运算符用于类类型对象时,必须转换为对应的重载函数调用,否则编译报错。
- 命名规范:重载函数名由
operator加运算符符号组成,如operator+。它和普通函数一样有返回类型、参数列表和函数体。 - 参数数量:重载函数的参数个数等于运算符作用的操作数数量。一元运算符一个参数,二元运算符两个参数。左侧传给第一个参数,右侧传给第二个。
- 成员函数特性:如果作为成员函数,第一个操作数默认通过隐式的
this指针传递。因此二元运算符作为成员函数时,只需声明一个参数(右操作数)。 - 优先级不变:重载后优先级和结合性与内置类型一致,且不能创建新符号(如
@)。 - 不可重载符号:
.、.*、::、sizeof、?:这五个运算符不能重载。 - 至少一个类类型:重载至少有一个参数必须是类类型,不能改变内置类型的含义(如
int + int)。 - 按需重载:只有重载后有意义的运算符才需要写。例如日期相减有意义,但日期相加通常无意义。
- 自增自减区分:前置 ++ 无参,后置 ++ 多一个
int形参以区分重载。 - 流运算符:
<<和>>建议重载为全局函数,避免this抢占左操作数位置,保证cout << obj的写法自然。
实战示例:日期类相等运算符
我们以日期类的相等运算符为例。返回值应为 bool,参数为另一个日期对象。由于是成员函数,this 指向当前对象,只需传入比较对象。
#include <iostream>
using namespace std;
class Date {
public:
// 构造函数
Date(int year = 2025, int month = 1, int day = 1) {
this->_year = year;
this->_month = month;
this->_day = day;
}
// 相等运算符重载
bool operator==(const Date& d) const {
return this->_year == d._year &&
this->_month == d._month &&
this->_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2025, 1, 1);
Date d2(2025, 1, 9);
// 直接使用运算符,编译器会将其转换为函数调用
if (d1 == d2) {
cout << "两个日期相等" << endl;
} else {
cout << "两个日期不相等" << endl;
}
return 0;
}
运行结果会显示不相等。这里的关键在于,虽然代码写的是 d1 == d2,但在底层汇编层面,它已经被编译器自动转换成了 d1.operator==(d2)。这种透明性让我们能像使用内置类型一样使用自定义类型。
注意:不能重载内置类型运算符。尝试对
int进行重载会导致编译错误,因为至少需要一个操作数是类类型。
二、赋值重载
赋值重载也是类的默认成员函数之一。如果不手动编写,编译器会生成一个默认的赋值函数。理解它的行为以及何时需要自己实现,是掌握类机制的关键。
默认生成的赋值重载行为
对于内置类型,默认赋值函数执行浅拷贝,即按字节复制。对于自定义类型,它会递归调用成员变量的赋值函数。这意味着如果类中包含指针成员,默认行为可能导致两个对象共享同一块堆内存,析构时引发重复释放错误。
判断是否需要手动实现赋值重载,可以参考以下技巧:
- 看析构/拷贝构造:如果写了析构函数或拷贝构造,通常意味着涉及资源管理,此时应配套实现赋值重载。
- 看堆空间:检查是否有内置类型成员变量指向堆空间(如
int*)。如果有,必须手动深拷贝。
手动实现赋值重载
以栈类(Stack)为例,它包含动态分配的数组。如果依赖默认赋值,两个栈对象将指向同一内存,修改一个会影响另一个,且析构时会崩溃。
我们需要手动实现深拷贝,并遵循以下原则:
- 成员函数:赋值运算符必须重载为成员函数。
- 参数引用:参数建议使用
const引用,避免拷贝并防止权限放大。 - 返回值:返回当前对象的引用(
*this),以支持连续赋值(如a = b = c)。 - 自赋值检查:必须检查
this != &other,防止给自己赋值导致内存泄漏或数据覆盖。
class Stack {
public:
Stack(int n = 10) {
this->_arr = (int*)malloc(n * sizeof(int));
if (this->_arr == nullptr) {
perror("malloc");
return;
}
this->_top = 0;
this->_capacity = n;
}
~Stack() {
if (this->_arr) free(this->_arr);
this->_top = this->_capacity = 0;
}
// 拷贝构造
Stack(const Stack& st) {
this->_arr = (int*)malloc(st._capacity * sizeof(int));
for (int i = 0; i < st._top; i++) {
this->_arr[i] = st._arr[i];
}
this->_top = st._top;
this->_capacity = st._capacity;
}
// 赋值重载
Stack& operator=(const Stack& st) {
if (this != &st) { // 自赋值检查
free(this->_arr); // 先释放原有资源
this->_arr = (int*)malloc(st._capacity * sizeof(int));
for (int i = 0; i < st._top; i++) {
this->_arr[i] = st._arr[i];
}
this->_top = st._top;
this->_capacity = st._capacity;
}
return *this;
}
void push(int x) {
this->_arr[this->_top++] = x;
}
private:
int* _arr;
int _top;
int _capacity;
};
int main() {
Stack st1;
st1.push(2);
st1.push(3);
Stack st2;
st2 = st1; // 调用赋值重载
return 0;
}
区分拷贝构造与赋值重载
两者容易混淆,核心区别在于对象是否已存在:
- 拷贝构造:对象正在被创建,用另一个对象初始化。例如
Date d2(d1);或Date d2 = d1;(注意:这里的=是初始化,不是赋值)。 - 赋值重载:对象已存在,将值赋给现有对象。例如
d2 = d1;。
调试时可以通过断点观察调用时机来确认。
三、取地址重载
取地址运算符 (&) 分为普通版本和 const 版本。编译器默认生成的版本足够日常使用,但在特殊场景下,我们可以自定义它们来控制访问权限。
例如,如果不想让外部轻易获取对象地址,可以返回空指针或特定地址。这常用于封装安全控制。
class Stack {
public:
// 普通取地址重载
Stack* operator&() {
return nullptr; // 拒绝获取地址
}
// const 取地址重载
Stack* operator&() const {
return nullptr;
}
};
int main() {
Stack st1;
Stack* ps = &st1; // 调用重载函数,ps 为 nullptr
return 0;
}
运行后会发现无法获取有效地址。除非有特殊的安全需求,否则建议直接使用编译器默认生成的版本。


