一、 概念
在C++中,拷贝构造函数用于用一个已存在的对象来初始化一个新对象。如果不显式定义,编译器会生成一个默认的拷贝构造函数。
-
浅拷贝(Shallow Copy):简单的赋值拷贝操作。编译器默认实现的拷贝构造函数就是浅拷贝。
二、 浅拷贝带来的灾难:内存重复释放
1. 代码场景
假设我们有一个 Person 类,其中 m_Age 是整型,而 m_Height 是一个指针(为什么用指针?为了将其放入堆中,这样可以手动控制其生命周期,比如传入较大的数据)。
class Person {
public:
int m_Age;
int *m_Height; // 身高,指针,指向堆区
// 有参构造函数
Person(int age, int height) {
m_Age = age;
m_Height = new int(height); // 在堆区开辟空间
}
// 析构函数:将堆区开辟的数据做释放操作
~Person() {
cout << "析构函数的调用" << endl;
if (m_Height != NULL) {
delete m_Height;
m_Height = NULL;
}
}
};
void test01() {
Person p1(18, 160);
cout << "p1的年龄:" << p1.m_Age << " 身高:" << *p1.m_Height << endl;
// 调用编译器默认的拷贝构造函数(浅拷贝)
Person p2(p1);
cout << "p2的年龄:" << p2.m_Age << " 身高:" << *p2.m_Height << endl;
}
2. 内存图解剖析(结合手写笔记)
当执行 Person p2(p1); 发生浅拷贝时,内存布局如下:
-
栈区:
-
p1在栈区(假设地址0x0044),m_Age为18,m_Height指针存储的地址是0x0011(指向堆区)。 -
p2在栈区(假设地址0x0041),m_Age为18,m_Height指针直接复制了p1的地址值0x0011。
-
-
堆区:
-
地址
0x0011处存放着160。 -
此时,
p1.m_Height和p2.m_Height同时指向了同一块堆区内存。
-
3. 问题爆发
在 test01 执行结束时,对象的析构顺序是先进后出(p2先析构,p1后析构):
-
p2先释放堆区内存:delete p2.m_Height;此时0x0011的内存被释放。 -
p1随后释放堆区内存:delete p1.m_Height;试图再次释放已经被释放的0x0011。 -
结果:引发 堆区内存重复释放(Double Free),程序崩溃(在笔记中被标注为“出现断点”)。
总结:浅拷贝带来的问题是堆区内存重复释放。解决方法:浅拷贝的问题要利用深拷贝解决。
三、 深拷贝的解决方案
为了解决浅拷贝带来的问题,我们必须自己实现拷贝构造函数,在堆区重新申请空间(深拷贝)。
1. 深拷贝构造函数实现
// 自己实现拷贝构造函数,解决浅拷贝带来的问题
Person(const Person &p) {
cout << "拷贝构造函数的调用" << endl;
m_Age = p.m_Age;
// 浅拷贝:编译器默认实现,会导致重复释放
// m_Height = p.m_Height;
// 深拷贝:重新在堆区申请空间
m_Height = new int(*p.m_Height); // 解引用,将p指向的值赋给新开辟的空间
}
2. 内存变化图解
实现深拷贝后,再次执行 Person p2(p1);:
-
堆区会新开辟一块空间(假设地址
0x0022),并将160存入。 -
p2.m_Height指向0x0022。 -
此时
p1和p2的指针完全独立。析构时,p2释放0x0022,p1释放0x0011,各自释放各自的内存,安全无副作用。
四、 完整代码与测试(补充)
结合笔记内容,完善的代码如下:
#include <iostream>
using namespace std;
class Person {
public:
int m_Age;
int *m_Height;
// 默认构造函数
Person() {
cout << "默认构造函数的调用" << endl;
}
// 有参构造函数
Person(int age, int height) {
m_Age = age;
m_Height = new int(height); // 指针在堆区申请内存
}
// 自定义拷贝构造函数(深拷贝)
Person(const Person &p) {
cout << "拷贝构造函数的调用" << endl;
m_Age = p.m_Age;
// m_Height = p.m_Height; // 浅拷贝,会导致堆区重复释放
m_Height = new int(*p.m_Height); // 深拷贝:重新分配堆内存
}
// 析构函数
~Person() {
cout << "析构函数的调用" << endl;
if (m_Height != NULL) {
delete m_Height;
m_Height = NULL;
}
}
};
void test01() {
Person p1(18, 160);
cout << "p1的年龄:" << p1.m_Age << " 身高:" << *p1.m_Height << endl;
Person p2(p1);
cout << "p2的年龄:" << p2.m_Age << " 身高:" << *p2.m_Height << endl;
}
int main() {
test01();
return 0;
}
五、 面试高频考点与最佳实践
-
口诀:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。
-
三法则(Rule of Three):如果一个类需要自定义析构函数,那么它通常也需要自定义拷贝构造函数和拷贝赋值运算符(
operator=)。因为需要析构通常意味着类管理了资源(如堆内存)。 -
现代C++建议:
-
在现代C++中,优先使用
std::string或std::vector等自带深拷贝语义的类,避免手动new/delete。 -
如果必须动态分配资源,推荐使用 智能指针(
std::unique_ptr或std::shared_ptr),它们能自动管理内存,从根本上规避浅拷贝带来的悬空指针和内存泄漏问题。
-
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2501_91257027/article/details/165364771



