5. 拷贝构造函数:什么是拷贝构造函数?浅拷贝与深拷贝、拷贝构造函数的调用时机
拷贝构造函数,说白了就是用一个已经存在的对象,去构造另一个新对象。我刚开始学C++时,总觉得这玩意儿有点多余——直接赋值不就行了?后来踩过几次坑才明白,拷贝构造函数背后藏着不少门道。
5.1 什么是拷贝构造函数
拷贝构造函数是一种特殊的构造函数。它的参数是同类型的常量引用。嗯,这里要注意,参数必须是引用,否则会无限递归调用自己——你想想看,传值本身就要调用拷贝构造,这不就死循环了吗?
class Person {
public:
// 拷贝构造函数
Person(const Person& other) {
name = other.name;
age = other.age;
}
private:
std::string name;
int age;
};
如果你不写,编译器会帮你生成一个默认的拷贝构造函数。它做的事情很简单:把每个成员变量逐个拷贝过去。这在大多数情况下够用了,但一旦涉及动态内存,问题就来了。
5.2 浅拷贝 vs 深拷贝
这两个概念,我当年理解了好一阵子。咱们直接看代码:
浅拷贝——复制指针,不复制数据
class ShallowArray {
public:
ShallowArray(int size) {
data = new int[size];
size_ = size;
}
// 默认拷贝构造函数——浅拷贝
// ShallowArray(const ShallowArray& other)
// : data(other.data), size_(other.size_) {}
~ShallowArray() {
delete[] data; // 两个对象指向同一块内存,会double free!
}
private:
int* data;
int size_;
};
浅拷贝只复制指针的值。两个对象的data指向同一块内存。一个对象析构时释放了内存,另一个对象再析构就崩溃了。我在项目中遇到过这种问题,排查了一下午才发现是浅拷贝惹的祸。
深拷贝——复制指针指向的数据
class DeepArray {
public:
DeepArray(int size) {
data = new int[size];
size_ = size;
}
// 深拷贝构造函数
DeepArray(const DeepArray& other) {
size_ = other.size_;
data = new int[size_]; // 重新分配内存
for (int i = 0; i < size_; ++i) {
data[i] = other.data[i]; // 逐个复制数据
}
}
~DeepArray() {
delete[] data;
}
private:
int* data;
int size_;
};
深拷贝会重新分配内存,然后把数据复制过去。每个对象都有自己的独立内存,互不干扰。这样析构时就不会出问题了。
核心区别一句话:浅拷贝复制指针地址,深拷贝复制指针指向的内容。
我曾经踩过的坑:写了一个字符串类,用了浅拷贝。两个字符串对象指向同一块字符数组,一个修改了内容,另一个也跟着变了。更惨的是析构时double free,程序直接崩溃。从那以后,凡是涉及动态内存的类,我第一件事就是检查拷贝构造函数。
5.3 拷贝构造函数的调用时机
搞清楚什么时候会调用拷贝构造函数,比背定义重要得多。我总结了三种常见场景:
| 场景 | 示例代码 | 说明 |
|---|---|---|
| 1. 用对象初始化另一个对象 | Person p2 = p1; |
最直接的调用方式 |
| 2. 函数传参(传值) | void func(Person p); |
形参是实参的拷贝 |
| 3. 函数返回对象(传值) | Person createPerson(); |
返回时创建临时对象 |
咱们看个完整例子:
#include <iostream>
#include <string>
class Person {
public:
Person(const std::string& name) : name_(name) {
std::cout << "普通构造: " << name_ << std::endl;
}
Person(const Person& other) : name_(other.name_) {
std::cout << "拷贝构造: " << name_ << std::endl;
}
~Person() {
std::cout << "析构: " << name_ << std::endl;
}
private:
std::string name_;
};
void printPerson(Person p) { // 场景2:传值调用拷贝构造
std::cout << "打印: " << std::endl;
}
Person createPerson() { // 场景3:返回对象
Person p("临时对象");
return p;
}
int main() {
Person p1("张三"); // 普通构造
Person p2 = p1; // 场景1:拷贝构造
printPerson(p2); // 场景2:传值拷贝
Person p3 = createPerson(); // 场景3:返回拷贝
return 0;
}
一个小技巧:如果你不想让对象被拷贝,可以把拷贝构造函数声明为delete。比如Person(const Person&) = delete;。这在设计单例类或资源独占类时特别有用。
5.4 知识体系总览
下面这张图把拷贝构造函数的要点串起来了:
这张图把拷贝构造函数的定义、浅拷贝与深拷贝的区别、以及三种调用时机都串起来了。我个人建议你把它保存下来,写代码时对照着看,思路会清晰很多。
总结一下:拷贝构造函数不是可有可无的。只要你的类管理了动态资源(堆内存、文件句柄、网络连接等),就必须实现深拷贝。否则,浅拷贝带来的问题会让你在调试时抓狂。我当年就因为这个,一个bug调了两天——嗯,从那以后我再也不敢偷懒了。
公众号:蓝海资料掘金营,微信deep3321