在 C++ 中,赋值运算符(=) 是最常用的运算符之一,默认情况下编译器会为类生成一个「浅拷贝赋值运算符」,但当类包含指针、动态内存、文件句柄等资源时,浅拷贝会导致「双重释放」「内存泄漏」等问题,因此需要手动重载赋值运算符,实现深拷贝。
赋值运算符重载是类的核心重载场景,且有严格的语法和语义要求,以下是详细讲解:
一、赋值运算符重载的核心特性
1、必须重载为成员函数:赋值运算符(=)不能重载为全局函数(编译器强制要求),因为左操作数必须是当前类对象;
2、默认浅拷贝的问题:编译器生成的默认赋值运算符仅拷贝成员变量的值(浅拷贝),若成员是指针,会导致两个对象指向同一块内存;
3、返回值为自身引用:支持链式赋值(如 a = b = c);
4、需处理自赋值:避免 a = a 时误释放自身资源;
5、遵循 "释放旧资源 → 分配新资源 → 拷贝数据" 的深拷贝逻辑。
二、重载语法(成员函数)
- 基础语法
cpp
// 类内声明
类名& operator=(const 类名& other);
// 类外定义
类名& 类名::operator=(const 类名& other) {
// 1. 处理自赋值(避免释放自身资源)
if (this == &other) {
return *this;
}
// 2. 释放当前对象的旧资源(避免内存泄漏)
// 例如:delete[] 指针成员;
// 3. 分配新资源并拷贝数据(深拷贝)
// 例如:指针成员 = new 类型[other.长度];
// 拷贝 other 的数据到新内存
// 4. 返回自身引用(支持链式赋值)
return *this;
}
- 关键说明
const 类名& other:用 const 保证不修改源对象,用引用避免拷贝,提升效率;
自赋值判断:this == &other 是必加逻辑,否则自赋值时会先释放自身资源,导致后续拷贝失败;
返回 类名&:必须返回自身引用(*this),否则链式赋值(a = b = c)会失效。
三、核心场景:深拷贝赋值(解决浅拷贝问题)
以包含动态字符串的 MyString 类为例,对比「默认浅拷贝」和「手动深拷贝赋值」的区别:
1、问题示例:默认浅拷贝的坑
cpp
#include <iostream>
#include <cstring>
using namespace std;
class MyString {
private:
char* str; // 动态分配的字符串指针
public:
// 构造函数
MyString(const char* s = "") {
str = new char[strlen(s) + 1];
strcpy(str, s);
}
// 析构函数(释放动态内存)
~MyString() {
delete[] str; // 浅拷贝时,两个对象会释放同一块内存 → 崩溃
}
// 打印字符串
void print() const {
cout << str << endl;
}
};
int main() {
MyString s1("Hello");
MyString s2;
s2 = s1; // 默认浅拷贝:s2.str 和 s1.str 指向同一块内存
s1.print(); // 正常
s2.print(); // 正常
return 0; // 析构时:s2先释放内存,s1再释放 → 双重释放,程序崩溃
}
- 解决:重载赋值运算符(深拷贝)
cpp
#include <iostream>
#include <cstring>
using namespace std;
class MyString {
private:
char* str;
int len; // 记录字符串长度,避免重复调用strlen
public:
// 构造函数
MyString(const char* s = "") {
len = strlen(s);
str = new char[len + 1];
strcpy(str, s);
}
// 析构函数
~MyString() {
delete[] str;
}
// 拷贝构造(配合赋值重载,完整深拷贝)
MyString(const MyString& other) {
len = other.len;
str = new char[len + 1];
strcpy(str, other.str);
}
// 重载赋值运算符(深拷贝)
MyString& operator=(const MyString& other) {
// 1. 处理自赋值
if (this == &other) {
return *this;
}
// 2. 释放当前对象的旧资源
delete[] str;
// 3. 深拷贝:分配新内存 + 拷贝数据
len = other.len;
str = new char[len + 1];
strcpy(str, other.str);
// 4. 返回自身引用
return *this;
}
void print() const {
cout << str << endl;
}
};
int main() {
MyString s1("Hello"), s2("World");
s2 = s1; // 调用重载的赋值运算符,深拷贝
s1.print(); // Hello
s2.print(); // Hello
MyString s3;
s3 = s2 = s1; // 链式赋值(支持)
s3.print(); // Hello
return 0; // 析构正常,无双重释放
}
四、进阶优化:拷贝并交换(Copy-and-Swap) Idiom
这是赋值运算符重载的经典优化写法,将「深拷贝」和「异常安全」结合,代码更简洁、健壮:
- 核心思路
先写一个 swap 成员函数(交换两个对象的资源);
赋值运算符接收「值传递」的参数(自动触发拷贝构造),再交换当前对象和临时对象的资源;
临时对象析构时自动释放旧资源,无需手动处理。 - 示例实现
cpp
#include <iostream>
#include <cstring>
#include <algorithm> // swap函数(可选)
using namespace std;
class MyString {
private:
char* str;
int len;
// 私有swap函数:交换两个对象的资源
void swap(MyString& other) {
// 交换指针和长度(无动态内存操作,高效)
std::swap(this->str, other.str);
std::swap(this->len, other.len);
}
public:
MyString(const char* s = "") {
len = strlen(s);
str = new char[len + 1];
strcpy(str, s);
}
// 拷贝构造
MyString(const MyString& other) {
len = other.len;
str = new char[len + 1];
strcpy(str, other.str);
}
// 析构函数
~MyString() {
delete[] str;
}
// 拷贝并交换版赋值运算符
MyString& operator=(MyString other) { // 值传递,自动拷贝
this->swap(other); // 交换当前对象和临时对象的资源
return *this; // 临时对象析构时释放旧资源
}
void print() const {
cout << str << endl;
}
};
int main() {
MyString s1("Copy-and-Swap"), s2;
s2 = s1;
s2.print(); // Copy-and-Swap
return 0;
}
- 优势
自动处理自赋值:值传递的临时对象是独立的,交换不会影响自身;
异常安全:拷贝构造若抛出异常(如内存分配失败),当前对象的资源不会被破坏;
代码简洁:无需手动释放旧资源、判断自赋值,逻辑更清晰。
五、复合赋值运算符重载(+=、-= 等)
复合赋值运算符(如 +=)的重载逻辑与 = 类似,但语义是「修改自身」,无需处理深拷贝的 "释放旧资源"(仅需追加数据):
cpp
#include <iostream>
#include <cstring>
using namespace std;
class MyString {
private:
char* str;
int len;
public:
MyString(const char* s = "") {
len = strlen(s);
str = new char[len + 1];
strcpy(str, s);
}
~MyString() {
delete[] str;
}
// 重载 +=(字符串拼接)
MyString& operator+=(const MyString& other) {
// 1. 计算新长度
int newLen = len + other.len;
// 2. 分配新内存
char* newStr = new char[newLen + 1];
// 3. 拷贝原有数据 + 追加新数据
strcpy(newStr, str);
strcat(newStr, other.str);
// 4. 释放旧内存
delete[] str;
// 5. 更新成员
str = newStr;
len = newLen;
// 6. 返回自身引用
return *this;
}
void print() const {
cout << str << endl;
}
};
int main() {
MyString s1("Hello"), s2(" World!");
s1 += s2; // 调用重载的 +=
s1.print(); // Hello World!
return 0;
}
六、常见误区与注意事项
1、忘记处理自赋值:
若省略 if (this == &other),自赋值时会先释放自身资源,导致后续拷贝数据时访问已释放的内存,程序崩溃。
2、返回值错误:
若返回 void,无法支持链式赋值(a = b = c 报错);
若返回值(MyString),会生成临时对象,效率低且违反语义(赋值应修改原对象)。
3、浅拷贝未重载:
类包含指针 / 动态内存时,必须重载赋值运算符(和拷贝构造),否则会导致双重释放、内存泄漏。
4、重载为全局函数:
赋值运算符不能重载为全局函数,编译器会直接报错(左操作数必须是类对象,且只能是成员函数)。
5、const 修饰错误:
赋值运算符的参数可加 const(保证不修改源对象),但函数本身不能加 const(因为要修改当前对象)。
七、禁用赋值运算符(可选)
若想禁止类对象的赋值操作(如单例类、包含不可拷贝资源的类),可将赋值运算符声明为 private 且不实现,或 C++11 后用 delete 修饰:
cpp
class NoAssign {
private:
// C++98:声明为private,不实现
NoAssign& operator=(const NoAssign&);
public:
NoAssign() {}
};
// C++11 更简洁的方式
class NoAssignC11 {
public:
NoAssignC11() {}
// 禁用赋值运算符
NoAssignC11& operator=(const NoAssignC11&) = delete;
// 可选:禁用拷贝构造
NoAssignC11(const NoAssignC11&) = delete;
};
int main() {
NoAssign a, b;
// a = b; // 编译错误(private)
NoAssignC11 c, d;
// c = d; // 编译错误(delete)
return 0;
}
总结
