C++ 深拷贝详解:拷贝构造函数与赋值运算符的完整实践
2026/8/3 7:19:43 网站建设 项目流程

1. 引言

在 C++ 中,当类包含指针成员并动态分配堆内存时,默认的拷贝行为往往会在不经意间埋下隐患:两个对象可能共享同一块堆内存,析构时重复释放,赋值时悄悄泄漏。深拷贝正是解决这些问题的关键手段。掌握深拷贝,不仅能帮助你写出内存安全、健壮可靠的 C++ 程序,更是深入理解对象生命周期与资源管理的必修课。本文将从浅拷贝的陷阱出发,系统讲解拷贝构造函数与赋值运算符的区别,并通过一个完整可运行的 Person 类示例,带你一步步实现正确的深拷贝。

2. 为什么需要深拷贝

当类中存在指针成员时,编译器默认生成的拷贝构造函数和赋值运算符执行的是浅拷贝,即只复制指针的值,而不是指针所指向的内容。这会导致两个对象共享同一块堆内存,带来两类典型问题:

flowchart TD subgraph 浅拷贝 A1[对象 p1 name 指针] -->|指向同一块堆内存| H1[堆内存 "Alice"] A2[对象 p2 name 指针] -->|指向同一块堆内存| H1 end subgraph 深拷贝 B1[对象 p1 name 指针] --> H2[堆内存 "Alice"] B2[对象 p2 name 指针] --> H3[堆内存 "Alice" 副本] end
  • 重复释放:两个对象析构时都会释放同一块内存,造成未定义行为甚至程序崩溃。
  • 内存泄漏:赋值时目标对象原有的堆内存没有被释放,导致内存泄漏。

因此,只要类中出现了new分配的内存,就必须手动重写拷贝构造函数和赋值运算符,实现深拷贝。

3. 拷贝构造函数与赋值运算符的区别

两者都用于复制对象,但调用时机、默认行为和深拷贝实现要点有明显差异,理解这些区别有助于正确重写深拷贝逻辑。

对比维度拷贝构造函数赋值运算符
调用时机用一个已存在的对象初始化一个新对象时调用,例如Person p2(p1);Person p2 = p1;对已存在的对象进行赋值时调用,例如p3 = p1;,此时p3已经构造完成
默认行为编译器会默认生成,但执行的是浅拷贝,多个对象会共享同一块堆内存,析构时容易造成重复释放编译器也会默认生成,但同样是浅拷贝,并且不会释放目标对象原有的堆内存,容易造成内存泄漏
深拷贝实现要点为新对象重新分配内存,再拷贝字符串内容,避免与源对象共享堆内存先判断是否自赋值,再释放目标对象原有的内存,然后重新分配并拷贝内容,最后返回*this以支持链式赋值

简单来说:拷贝构造函数负责“从无到有”地创建新对象,赋值运算符负责“从有到新”地更新已有对象,因此赋值运算符必须额外处理旧内存的释放和自赋值问题。

4. 完整示例:Person 类的深拷贝实现

下面给出一个完整、可运行的 C++ 示例,包含深拷贝构造函数和深拷贝赋值运算符,并标注了关键步骤。

#include <iostream> #include <cstring> using namespace std; class Person { public: // 构造函数:为 name 分配内存 Person(const char* n, int a, char s) : age(a), sex(s) { name = new char[strlen(n) + 1]; strcpy(name, n); } // 深拷贝构造函数:为新对象重新分配内存 Person(const Person& rhs) { name = new char[strlen(rhs.name) + 1]; // 关键:重新分配内存 strcpy(name, rhs.name); // 再拷贝内容 age = rhs.age; sex = rhs.sex; } // 深拷贝赋值运算符:先释放旧内存,再重新分配 Person& operator=(const Person& rhs) { if (this == &rhs) { // 防止自赋值 return *this; } delete[] name; // 释放旧内存 name = new char[strlen(rhs.name) + 1]; // 重新分配 strcpy(name, rhs.name); // 拷贝内容 age = rhs.age; sex = rhs.sex; return *this; } // 析构函数:释放内存 ~Person() { delete[] name; } void show() const { cout << name << ", " << age << ", " << sex << endl; } private: char* name; int age; char sex; }; int main() { Person p1("Alice", 25, 'F'); Person p2(p1); // 调用深拷贝构造函数 Person p3("Bob", 30, 'M'); p3 = p1; // 调用深拷贝赋值运算符 p1.show(); p2.show(); p3.show(); return 0; }

关键步骤说明:

  • 深拷贝构造函数:先为name重新分配内存,再拷贝字符串内容,避免两个对象共享同一块堆内存。
  • 深拷贝赋值运算符:先判断是否自赋值,再释放旧内存、重新分配并拷贝,最后返回*this以支持链式赋值。
  • 析构函数:释放name指向的堆内存,防止内存泄漏。

5. 常见误区与注意事项

在实际开发中,深拷贝的实现有几个容易踩坑的地方,需要特别留意:

  • 自赋值判断:赋值运算符必须先判断this == &rhs,否则自赋值时会先释放自身内存,再访问已释放的内存,导致未定义行为。
  • 异常安全:更稳妥的做法是先分配新内存并拷贝成功后再释放旧内存,避免new抛出异常时对象处于不一致状态。下面给出一个异常安全的赋值运算符实现:
Person& operator=(const Person& rhs) { if (this == &rhs) { // 防止自赋值 return *this; } // 先分配新内存并拷贝成功,再释放旧内存 char* new_name = new char[strlen(rhs.name) + 1]; // 若 new 失败,抛出异常,对象状态不变 strcpy(new_name, rhs.name); // 拷贝内容 delete[] name; // 拷贝成功后再释放旧内存 name = new_name; // 最后更新指针 age = rhs.age; sex = rhs.sex; return *this; // 返回 this 指针指向的对象,支持链式赋值 }
  • 返回引用:赋值运算符应返回Person&而不是void,以支持p1 = p2 = p3这样的链式赋值。
  • 临时对象优化:现代 C++ 编译器会对Person p4(Person(...))这类临时对象构造进行优化,直接构造新对象,减少不必要的拷贝开销。

6. 总结

深拷贝是 C++ 面向对象编程中必须掌握的核心技能。当类包含指针成员时,务必重写拷贝构造函数和赋值运算符,并配套实现析构函数,遵循“三法则”。拷贝构造函数负责创建新对象,赋值运算符负责更新已有对象,两者在实现细节上各有侧重。掌握这些要点,就能有效避免浅拷贝带来的内存问题,写出健壮的 C++ 代码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询