在C++中,拷贝构造函数(Copy Constructor)是一种特殊的构造函数,它用于用一个已存在的同类对象来初始化另一个正在创建的新对象。
一、 拷贝构造函数是什么?
1. 定义与形式
它的参数是本类对象的引用 (通常加 const 修饰,防止原对象被修改)。其标准签名如下:
cpp
class MyClass {
public:
//自定义的拷贝构造函数
MyClass(const MyClass& other){}
//有了拷贝构造函数,就必须要自己写默认的构造函数了,不然就没办法MyClass obj1这样定义类对象了
MyClass() {
// 可以在这里初始化成员变量
}
};
2. 什么时候会被自动调用?
拷贝构造函数在以下三种"对象初始化"的场景下会被隐式调用:
-
用一个对象初始化另一个新对象:
cpp#include "MyClass.h" int main() { MyClass obj1; MyClass obj2 = obj1; // 或 MyClass obj2(obj1); return 0; } -
函数参数以值传递(Pass by Value)方式传入对象:
cppvoid func(MyClass obj); MyClass obj1; func(obj1); // 这里会调用拷贝构造函数将 obj1 复制一份传进函数 -
函数返回值是以值传递方式返回一个对象:
cppMyClass createObj() { MyClass temp; return temp; // 可能触发拷贝(或被编译器优化为移动/返回值优化 RVO) }
二、 为什么要有拷贝构造函数的存在?
如果没有显式定义拷贝构造函数,C++ 编译器会为我们自动生成一个默认的拷贝构造函数。那么,为什么语言本身需要这样一个机制,以及为什么有时我们必须自己写它呢?
1. 核心原因:实现对象的安全复制
当我们要把一个对象的所有状态(成员变量)复制给另一个新对象时,拷贝构造函数提供了一种标准、规范的方式。它保证了对象在创建时就能获得与源对象相同初始状态。
2. 区分"浅拷贝"与"深拷贝"(为什么有时需要自己写)
编译器自动生成的默认拷贝构造函数执行的是浅拷贝(Shallow Copy)------它只是按字节(逐个成员)把原对象的属性值复制到新对象中。
-
浅拷贝的隐患(指针/动态内存) : 如果类里面包含指针,且指针指向了堆区动态分配的内存,浅拷贝会导致两个对象的指针指向同一块内存地址。
-
当其中一个对象被销毁并释放了该内存时,另一个对象的指针就会变成悬空指针(Dangling Pointer)。
-
当两个对象先后析构时,会试图对同一块内存释放两次(Double Free),导致程序崩溃。
-
-
解决方案------深拷贝(Deep Copy) : 正因为浅拷贝在遇到动态资源管理时的致命缺陷,我们需要自定义拷贝构造函数 来实行深拷贝:不仅复制指针的值,还要为新对象申请独立的内存空间,并把原内存里的内容复制过去。
三、 代码对比示例
浅拷贝引发的问题(编译器默认行为):
cpp
#include <iostream>
#include <cstring>
#include <windows.h> // 必须包含这个头文件,使vscode支持中文输出
class Shallow {
public:
int* data;
Shallow(int d) {
data = new int(d); // 申请堆内存
}
// 什么都不写才是默认的拷贝构造函数(浅拷贝)
//如果这样写了,哪怕函数体是空,也不是默认的构造函数
/**Shallow(const Shallow& other) {
}**/
~Shallow() {
std::cout << "[准备析构] this=" << this << " data=" << data << "\n";
delete data; // 析构时释放内存
}
};
int main() {
// 设置控制台输出编码为 UTF-8
SetConsoleOutputCP(65001);
Shallow a(10);
{
Shallow b = a; // b 和 a 共享内存
std::cout << "*(a.data)=" << *(a.data) << " \n";
std::cout << "*(b.data)=" << *(b.data) << " \n";
} // b 析构,释放 data
std::cout << " b 析构后, a.data=" << a.data << " *a.data=" << *(a.data) << "\n";
return 0;
}
运行结果
cpp
*(a.data)=10
*(b.data)=10
[准备析构] this=0xb345fffbd0 data=0x2710cd94a50
b 析构后, a.data=0x2710cd94a50 *a.data=215569920
[准备析构] this=0xb345fffbd8 data=0x2710cd94a50
由上述代码可以看到,浅拷贝在类存在指针成员变量的时候,如果使用了默认的拷贝构造方法初始化另一个对象,有可能会在对象析构时对同一块内存释放两次
深拷贝(自定义拷贝构造函数):
cpp
#include <iostream>
#include <windows.h> // 必须包含这个头文件,使vscode支持中文输出
class Deep {
public:
int* data;
Deep(int d) {
data = new int(d);
}
// 自定义拷贝构造函数(深拷贝)
Deep(const Deep& other) {
std::cout << "执行深拷贝构造,为新对象重新申请独立的内存,并复制值\n";
data = new int(*(other.data));
}
~Deep() {
std::cout << "[准备析构] this=" << this << " data=" << data << "\n";
delete data; // 各自释放各自的内存,互不影响
}
};
int main() {
// 设置控制台输出编码为 UTF-8
SetConsoleOutputCP(65001);
Deep a(10);
{
Deep b = a; // b 和 a 共享内存
std::cout << "*(a.data)=" << *(a.data) << " \n";
std::cout << "*(b.data)=" << *(b.data) << " \n";
} // b 析构,释放 data
std::cout << "after b dtor, a.data=" << a.data << " *a.data=" << *a.data << "\n";
return 0; // 安全退出
}
运行结果
cpp
执行深拷贝构造,为新对象重新申请独立的内存,并复制值
*(a.data)=10
*(b.data)=10
[准备析构] this=0xfc07ff6e0 data=0x14e59038d70
after b dtor, a.data=0x14e59038d50 *a.data=10
[准备析构] this=0xfc07ff6e8 data=0x14e59038d50
可以看到,程序不会再出现 会对同一块内存释放两次和卡了一小会的问题,两次析构的是不一样的内存
总结
-
是什么:用已有对象初始化新对象的特殊构造函数。
-
为什么要有 :为了能够复制对象。如果类中只包含基本数据类型(如
int、double),编译器默认的浅拷贝就够用了;但如果类中含有指针、文件句柄、网络连接等动态资源 ,就需要通过自定义拷贝构造函数来实现深拷贝,以保证内存安全和程序的健壮性(这也是C++三大件------拷贝构造、赋值运算符重载、析构函数需要配合"Rule of Three/Five"原则编写的原因)
ps:简单补充一下什么是Rule of Three/Five"原则
Rule of Three(三法则,C++11 之前)
如果你需要自定义以下三者中的任意一个,那几乎肯定也需要定义另外两个:
-
析构函数
-
拷贝构造函数
-
拷贝赋值运算符
Rule of Five(五法则,C++11 及以后)
C++11 引入了移动语义,新增了两个特殊成员函数。五法则建议,如果你需要定义(或 =delete)以下五个中的任意一个,就应该考虑全部五个:
-
析构函数
-
拷贝构造函数
-
拷贝赋值运算符
-
移动构造函数 (C++11)
-
移动赋值运算符 (C++11)
现代首选:Rule of Zero(零法则)
虽然三/五法则很重要,但在现代 C++ 中,最好的做法通常是"零法则" :不要自己手动管理资源。