C++ 拷贝构造函数与赋值运算符:深拷贝与浅拷贝辨析
深入解析 C++ 中拷贝构造函数与赋值运算符的区别及调用场景。阐述了浅拷贝在指针成员存在时导致的内存重复释放风险,并详细演示了如何通过手动实现深拷贝构造函数和重载赋值运算符来解决该问题。文章结合 String 类和 MyArray 类的代码示例,讲解了三法则原则、自赋值处理及智能指针替代方案,旨在帮助开发者避免野指针和内存泄漏,确保对象内存管理的独立性与安全性。

深入解析 C++ 中拷贝构造函数与赋值运算符的区别及调用场景。阐述了浅拷贝在指针成员存在时导致的内存重复释放风险,并详细演示了如何通过手动实现深拷贝构造函数和重载赋值运算符来解决该问题。文章结合 String 类和 MyArray 类的代码示例,讲解了三法则原则、自赋值处理及智能指针替代方案,旨在帮助开发者避免野指针和内存泄漏,确保对象内存管理的独立性与安全性。

掌握拷贝构造函数与赋值运算符的定义及调用场景,理解深拷贝与浅拷贝的本质区别,能够在实际开发中避免内存泄漏与野指针问题。
结论:拷贝构造函数是一种特殊的构造函数,用于通过一个已存在的对象创建一个新对象,其参数必须是本类对象的常量引用(const 类名&)。
class Person {
public:
// 普通构造函数
Person(参数列表);
// 拷贝构造函数
Person(const Person& other);
};
注意事项:
const 防止实参被修改,使用引用避免无限递归调用拷贝构造函数。拷贝构造函数在以下三种场景下会被自动调用:
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
string name;
int age;
// 普通构造函数
Person(string n, int a) : name(n), age(a) {
cout << "普通构造函数被调用" << endl;
}
// 拷贝构造函数
Person(const Person& other) {
this->name = other.name;
this->age = other.age;
cout << "拷贝构造函数被调用" << endl;
}
};
// 场景 2:函数参数为类对象(值传递)
void func(Person p) {
cout << "函数内对象姓名:" << p.name << endl;
}
// 场景 3:函数返回值为类对象(值传递)
Person getPerson() {
Person p("王五", 30);
return p;
}
int main() {
// 普通构造创建对象
Person p1("张三", 20);
// 场景 1:使用 p1 初始化 p2
Person p2 = p1;
// 场景 2:值传递传递对象
func(p1);
// 场景 3:值传递返回对象
Person p3 = getPerson();
return 0;
}
普通构造函数被调用
拷贝构造函数被调用
拷贝构造函数被调用
函数内对象姓名:张三
普通构造函数被调用
拷贝构造函数被调用
浅拷贝是指仅拷贝对象的成员变量值,深拷贝是指不仅拷贝成员变量值,还为指针成员重新分配内存并拷贝数据,二者的核心差异体现在指针成员的处理上。
默认拷贝构造函数和默认赋值运算符实现的是浅拷贝,当类中包含指针成员时,浅拷贝会导致多个对象的指针指向同一块内存,引发严重问题。
#include <iostream>
#include <cstring>
using namespace std;
class String {
private:
char* str; // 指针成员
public:
// 普通构造函数:分配堆内存
String(const char* s = "") {
str = new char[strlen(s) + 1];
strcpy(str, s);
cout << "普通构造函数:分配内存" << endl;
}
// 析构函数:释放堆内存
~String() {
delete[] str;
cout << "析构函数:释放内存" << endl;
}
// 打印字符串
void show() {
cout << str << endl;
}
};
int main() {
String s1("Hello C++");
// 浅拷贝:s2.str 与 s1.str 指向同一块内存
String s2 = s1;
s1.show();
s2.show();
return 0;
}
普通构造函数:分配内存
Hello C++
Hello C++
析构函数:释放内存
析构函数:释放内存
问题 1:重复释放内存
s1 和 s2 的指针指向同一块堆内存。问题 2:修改一个对象影响另一个
s1.str 指向的内容,s2.str 的内容也会被改变,违背对象的独立性。深拷贝的核心是为指针成员重新分配内存,并将原对象指针指向的数据拷贝到新内存中,从而保证每个对象的指针成员都有独立的内存空间。
#include <iostream>
#include <cstring>
using namespace std;
class String {
private:
char* str;
public:
String(const char* s = "") {
str = new char[strlen(s) + 1];
strcpy(str, s);
cout << "普通构造函数:分配内存" << endl;
}
// 手动实现深拷贝构造函数
String(const String& other) {
// 为新对象分配独立内存
this->str = new char[strlen(other.str) + 1];
// 拷贝数据
strcpy(this->str, other.str);
cout << "深拷贝构造函数:分配独立内存" << endl;
}
~String() {
delete[] str;
cout << "析构函数:释放内存" << endl;
}
void show() {
cout << str << endl;
}
// 提供修改字符串的方法,验证独立性
void setStr(const char* s) {
delete[] str;
str = new char[(s) + ];
(str, s);
}
};
{
;
String s2 = s1;
cout << << endl;
s();
s();
s();
cout << << endl;
s();
s();
;
}
普通构造函数:分配内存
深拷贝构造函数:分配独立内存
修改前:
Hello C++
Hello C++
修改后:
Hello Deep Copy
Hello C++
析构函数:释放内存
析构函数:释放内存
核心优势
s1 和 s2 的指针指向不同的内存空间,修改一个对象不会影响另一个。赋值运算符(=)的默认行为也是浅拷贝,当类中包含指针成员时,必须手动重载赋值运算符并实现深拷贝,其实现逻辑与深拷贝构造函数类似,但需要处理自赋值问题。
类名& operator=(const 类名& other) {
// 1. 处理自赋值
if (this == &other) {
return *this;
}
// 2. 释放当前对象的原有内存
delete[] this->指针成员;
// 3. 分配新内存并拷贝数据
this->指针成员 = new 类型 [大小];
// 拷贝数据逻辑
// 4. 返回当前对象的引用,支持链式赋值
return *this;
}
a = a 这种情况导致内存提前释放。a = b = c)。#include <iostream>
#include <cstring>
using namespace std;
class String {
private:
char* str;
public:
String(const char* s = "") {
str = new char[strlen(s) + 1];
strcpy(str, s);
cout << "普通构造函数:分配内存" << endl;
}
// 深拷贝构造函数
String(const String& other) {
this->str = new char[strlen(other.str) + 1];
strcpy(this->str, other.str);
cout << "深拷贝构造函数:分配独立内存" << endl;
}
// 重载赋值运算符,实现深拷贝
String& operator=(const String& other) {
// 1. 处理自赋值
if (this == &other) {
return *this;
}
// 2. 释放当前对象的原有内存
delete[] this->str;
// 3. 分配新内存并拷贝数据
this->str = new char[strlen(other.str) + 1];
strcpy(->str, other.str);
cout << << endl;
*;
}
~() {
[] str;
cout << << endl;
}
{
cout << str << endl;
}
};
{
;
String s2;
s2 = s1;
s();
s();
String s3;
s3 = s2 = s1;
cout << ;
s();
;
}
普通构造函数:分配内存
普通构造函数:分配内存
赋值运算符重载:深拷贝
Hello C++
Hello C++
赋值运算符重载:深拷贝
链式赋值后 s3:Hello C++
析构函数:释放内存
析构函数:释放内存
析构函数:释放内存
核心区别总结:拷贝构造函数用于创建新对象,赋值运算符用于给已存在的对象赋值,二者的调用时机和执行逻辑完全不同。
| 特性 | 拷贝构造函数 | 赋值运算符 |
|---|---|---|
| 调用时机 | 用已有对象创建新对象时调用 | 给已存在的对象赋值时调用 |
| 参数要求 | 必须是 const 类名& | 通常是 const 类名& |
| 内存操作 | 分配新内存,无原有内存需要释放 | 需先释放当前对象原有内存 |
| 返回值 | 无返回值(构造函数特性) | 必须返回 类名&,支持链式赋值 |
| 默认实现 | 默认浅拷贝 | 默认浅拷贝 |
需求:设计一个自定义数组类 MyArray,支持动态扩容,要求实现深拷贝构造函数和赋值运算符重载,避免浅拷贝导致的内存问题。
int* arr(存储数组数据)、int size(数组大小)。#include <iostream>
#include <cstring>
using namespace std;
class MyArray {
private:
int* arr; // 动态数组指针
int size; // 数组大小
public:
// 构造函数:创建指定大小的数组
MyArray(int s = 0) : size(s) {
if (size > 0) {
arr = new int[size];
// 初始化数组元素为 0
memset(arr, 0, sizeof(int) * size);
} else {
arr = nullptr;
}
cout << "构造函数:创建大小为 " << size << " 的数组" << endl;
}
// 深拷贝构造函数
MyArray(const MyArray& other) {
this->size = other.size;
if (this->size > 0) {
// 分配独立内存
this->arr = new int[this->size];
// 拷贝数组数据
for (int i = 0; i < this->size; i++) {
this->arr[i] = other.arr[i];
}
} {
->arr = ;
}
cout << << size << << endl;
}
MyArray& =( MyArray& other) {
( == &other) {
*;
}
(->arr != ) {
[] ->arr;
}
->size = other.size;
(->size > ) {
->arr = [->size];
( i = ; i < ->size; i++) {
->arr[i] = other.arr[i];
}
} {
->arr = ;
}
cout << << endl;
*;
}
{
(index >= && index < size) {
arr[index] = value;
} {
cout << << endl;
}
}
{
(arr == ) {
cout << << endl;
;
}
cout << ;
( i = ; i < size; i++) {
cout << arr[i] << ;
}
cout << endl;
}
~() {
(arr != ) {
[] arr;
arr = ;
}
cout << << endl;
}
};
{
;
arr(, );
arr(, );
arr(, );
arr();
MyArray arr2 = arr1;
arr(, );
cout << << endl;
arr();
arr();
;
arr3 = arr1;
arr(, );
cout << << endl;
arr();
arr();
;
}
构造函数:创建大小为 5 的数组
数组元素:10 20 30 0 0
深拷贝构造函数:拷贝大小为 5 的数组
修改 arr2 后:
数组元素:10 20 30 0 0
数组元素:100 20 30 0 0
构造函数:创建大小为 3 的数组
赋值运算符重载:深拷贝数组
修改 arr3 后:
数组元素:10 20 30 0 0
数组元素:10 200 30 0 0
析构函数:释放数组内存
析构函数:释放数组内存
析构函数:释放数组内存
unique_ptr、shared_ptr 等智能指针替代裸指针,自动管理内存,避免手动实现深拷贝。解决方案:在赋值运算符重载函数开头,添加 if (this == &other) 判断,直接返回 *this。
解决方案:析构函数或赋值运算符中释放内存后,将指针置为 nullptr,避免野指针。
解决方案:可以添加异常处理逻辑,捕获内存分配失败的异常,增强程序健壮性。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online