🔄 C++ 赋值运算符重载:深拷贝 vs 浅拷贝的生死线!
大家好!今天我们来聊一个 C++ 中极易被忽视、却可能引发严重 bug 的知识点------赋值运算符 operator= 的重载。
你可能写过 a = b,但当你的类中包含指向堆内存的指针时,这个看似简单的等号,就可能让你的程序崩溃、内存泄漏,甚至"神秘地"修改不该改的数据!
别慌,今天我们就用一段经典示例,彻底搞懂 为什么需要重载赋值运算符 ,以及 如何正确实现深拷贝。
🧠 编译器默认给你的 4 个函数
在 C++ 中,即使你什么都没写,编译器也会悄悄为你的类生成以下 4 个函数:
-
默认构造函数(无参,空实现)
-
默认析构函数(无参,空实现)
-
默认拷贝构造函数(逐成员值拷贝)
-
**默认赋值运算符
operator=**(也是逐成员值拷贝)
⚠️ 问题来了:"值拷贝"对指针来说,就是"浅拷贝"!
💥 浅拷贝的灾难:多个对象共用一块堆内存
来看你写的 Person 类(代码原样保留,未作任何修改):
cpp
class Person
{
public:
Person(int age)
{
// 将年龄数据开辟到堆区
m_Age = new int(age);
}
// 重载赋值运算符
Person& operator=(Person &p)
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
// 编译器提供的代码是浅拷贝
// m_Age = p.m_Age;
// 提供深拷贝 解决浅拷贝的问题
m_Age = new int(*p.m_Age);
// 返回自身
return *this;
}
~Person()
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
}
// 年龄的指针
int *m_Age;
};
如果不重载 operator=,会发生什么?
假设使用默认赋值:
cpp
p2 = p1; // 默认:m_Age = p1.m_Age (浅拷贝!)
结果:
-
p1.m_Age和p2.m_Age指向同一块堆内存 -
当
p1或p2析构时,delete这块内存 -
另一个对象再访问或析构 → 野指针 / 重复释放 → 程序崩溃!
这就是典型的浅拷贝陷阱。
✅ 正确做法:手动实现深拷贝
你的重载版本完美解决了这个问题:
cpp
Person& operator=(Person &p)
{
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
m_Age = new int(*p.m_Age); // 深拷贝:新开内存,复制值
return *this;
}
关键步骤:
-
先释放自身原有堆内存(防止内存泄漏)
-
从源对象的堆数据中读取值,重新 new 一块新内存
-
返回
*this的引用 ,支持链式赋值(如p3 = p2 = p1)
🧪 测试效果
cpp
void test01()
{
Person p1(18);
Person p2(20);
Person p3(30);
p3 = p2 = p1; // 链式赋值
cout << "p1的年龄为:" << *p1.m_Age << endl;
cout << "p2的年龄为:" << *p2.m_Age << endl;
cout << "p3的年龄为:" << *p3.m_Age << endl;
}
输出:
cpp
p1的年龄为:18
p2的年龄为:18
p3的年龄为:18
✅ 三个对象各自拥有独立的堆内存,互不影响!
✅ 支持 p3 = p2 = p1 链式赋值(因为返回了 *this 引用)!
📌 黄金法则:三/五法则(Rule of Three/Five)
如果你的类中:
-
使用了 动态内存 (如
new) -
或管理了 其他资源(文件句柄、socket 等)
那么你很可能需要同时自定义:
-
析构函数
-
拷贝构造函数
-
赋值运算符
这就是著名的 "三法则"(C++11 后扩展为"五法则",加上移动构造和移动赋值)
否则,默认的浅拷贝会让你陷入万劫不复的调试深渊!
✅ 总结
-
编译器自动生成的
operator=是浅拷贝,对指针极其危险。 -
当类中有堆区指针时,必须重载赋值运算符 ,实现深拷贝。
-
记得:先释放旧资源,再分配新资源,最后返回
*this。 -
支持链式赋值的关键:返回引用!
如果你觉得这篇推文帮你避开了一个大坑,欢迎点赞、收藏、转发!
也欢迎留言:"你在项目中遇到过浅拷贝导致的 bug 吗?"