当前位置: 首页 > news >正文

【c++面向对象编程】第4篇:类与对象(三):拷贝构造函数与深浅拷贝问题

目录

一、一个崩溃的程序

二、拷贝构造函数是什么?

调用时机(三个场景)

三、浅拷贝 vs 深拷贝

浅拷贝(默认行为)

深拷贝(正确的做法)

四、什么时候必须自己写拷贝构造函数?

一个反面例子:vector的浅拷贝问题

五、完整的例子:安全的动态数组

六、三个常见的坑

1. 拷贝构造函数参数不用引用 → 无限递归

2. 忘了const导致无法拷贝const对象

3. 浅拷贝发生在你没想到的地方

七、这一篇的收获


一、一个崩溃的程序

先看这段代码,你觉得它会崩溃吗?

cpp

class StringWrapper { private: char* data; public: StringWrapper(const char* str) { data = new char[strlen(str) + 1]; strcpy(data, str); } ~StringWrapper() { delete[] data; } void print() { cout << data << endl; } }; int main() { StringWrapper s1("Hello"); StringWrapper s2 = s1; // 用s1初始化s2 s1.print(); s2.print(); return 0; } // 程序在这里崩溃!

运行结果:可能正常输出,也可能输出乱码,最后大概率崩溃

原因很简单:s1s2里面的data指针指向了同一块内存。当程序结束,s2先析构,delete[]了那块内存;然后s1析构,再次delete[]同一块内存——重复释放,程序崩溃

这就是浅拷贝带来的灾难。


二、拷贝构造函数是什么?

拷贝构造函数是一种特殊的构造函数:

  • 参数是本类对象的const引用

  • 已有的对象去创建新的对象时自动调用

  • 如果你不写,编译器会生成一个默认的(逐成员复制)

语法长这样:

cpp

class MyClass { public: // 拷贝构造函数 MyClass(const MyClass& other) { // 拷贝逻辑 } };

调用时机(三个场景)

cpp

class Demo { public: Demo() { cout << "普通构造" << endl; } Demo(const Demo& other) { cout << "拷贝构造" << endl; } ~Demo() { cout << "析构" << endl; } }; Demo makeDemo() { Demo d; return d; // 场景3:返回值 } int main() { Demo a; // 普通构造 Demo b = a; // 场景1:用a初始化b → 拷贝构造 Demo c(a); // 场景2:直接传参 → 拷贝构造 Demo d = makeDemo(); // 场景3:返回值(可能被优化掉,不一定调用) }

关键点:“=”在这里不是赋值,是初始化。赋值是后面讲的重载operator=


三、浅拷贝 vs 深拷贝

浅拷贝(默认行为)

编译器生成的默认拷贝构造函数做的事很简单:把每个成员变量的值原样复制

cpp

// 编译器生成的默认版本(概念上) StringWrapper(const StringWrapper& other) : data(other.data) // 只复制指针的值,不复制指针指向的内容 {}

对于intdouble这种值类型,浅拷贝没问题。但对于指针,复制的是地址,不是地址里的内容。

浅拷贝的问题

  • 两个对象指向同一块内存

  • 一个修改,另一个也跟着变(可能不是你想要的效果)

  • 一个释放,另一个变成悬空指针

  • 重复释放导致崩溃

深拷贝(正确的做法)

深拷贝的做法:不复制指针的值,而是复制指针指向的内容

cpp

class StringWrapper { private: char* data; public: // 普通构造函数 StringWrapper(const char* str) { data = new char[strlen(str) + 1]; strcpy(data, str); } // 拷贝构造函数(深拷贝) StringWrapper(const StringWrapper& other) { // 1. 分配新内存 data = new char[strlen(other.data) + 1]; // 2. 复制内容 strcpy(data, other.data); cout << "深拷贝:" << data << endl; } // 析构函数 ~StringWrapper() { delete[] data; cout << "释放:" << data << endl; } void print() { cout << data << endl; } // 后面会讲赋值运算符重载 };

现在运行之前会崩溃的例子:

cpp

int main() { StringWrapper s1("Hello"); StringWrapper s2 = s1; // 深拷贝:s2有自己独立的内存 s1.print(); // Hello s2.print(); // Hello return 0; // 分别释放两块内存,不冲突 }

内存布局对比:

text

浅拷贝: s1.data ──→ [H][e][l][l][o][\0] s2.data ──→ ↑ (指向同一块) 深拷贝: s1.data ──→ [H][e][l][l][o][\0] s2.data ──→ [H][e][l][l][o][\0] (另一块内存)

四、什么时候必须自己写拷贝构造函数?

三法则(Rule of Three):如果类需要自定义析构函数,那么它几乎一定也需要自定义拷贝构造函数和拷贝赋值运算符。

具体来说,以下情况必须写拷贝构造函数:

  1. 类里有指针成员,并且构造函数里用new分配了内存

  2. 类里有文件句柄、数据库连接等需要“独占”的资源

  3. 类里有互斥锁(mutex)(两个对象拥有同一个锁会导致死锁)

一句话:默认的逐成员复制对你的资源管理方式不适用时

一个反面例子:vector的浅拷贝问题

cpp

class IntVector { private: int* arr; int size; public: IntVector(int n) : size(n) { arr = new int[n]; for(int i=0; i<n; i++) arr[i] = i; } ~IntVector() { delete[] arr; } // 没有写拷贝构造函数 → 浅拷贝! void set(int idx, int val) { arr[idx] = val; } int get(int idx) { return arr[idx]; } }; int main() { IntVector v1(5); IntVector v2 = v1; // 浅拷贝,v2.arr指向v1.arr同一块内存 v2.set(0, 999); // 修改v2 cout << v1.get(0); // 输出999!v1被意外修改了 // 程序结束,两次delete[]同一块内存 → 崩溃 }

这就是所谓的“意外的共享状态”。


五、完整的例子:安全的动态数组

cpp

#include <iostream> #include <cstring> using namespace std; class SafeArray { private: int* data; int size; public: // 普通构造函数 SafeArray(int n) : size(n) { data = new int[n]; for (int i = 0; i < n; i++) { data[i] = 0; } cout << "构造:分配了" << n << "个int" << endl; } // 拷贝构造函数(深拷贝) SafeArray(const SafeArray& other) : size(other.size) { data = new int[size]; for (int i = 0; i < size; i++) { data[i] = other.data[i]; } cout << "拷贝构造:深拷贝了" << size << "个int" << endl; } // 析构函数 ~SafeArray() { delete[] data; cout << "析构:释放了" << size << "个int" << endl; } void set(int idx, int val) { if (idx >= 0 && idx < size) data[idx] = val; } int get(int idx) const { if (idx >= 0 && idx < size) return data[idx]; return -1; } void print() const { cout << "["; for (int i = 0; i < size; i++) { cout << data[i] << (i < size-1 ? ", " : ""); } cout << "]" << endl; } }; int main() { SafeArray a(5); for (int i = 0; i < 5; i++) a.set(i, i * 10); a.print(); // [0, 10, 20, 30, 40] SafeArray b = a; // 拷贝构造 b.set(0, 999); cout << "a: "; a.print(); // [0, 10, 20, 30, 40] ← 没被影响 cout << "b: "; b.print(); // [999, 10, 20, 30, 40] ← 独立修改 return 0; }

输出:

text

构造:分配了5个int [0, 10, 20, 30, 40] 拷贝构造:深拷贝了5个int a: [0, 10, 20, 30, 40] b: [999, 10, 20, 30, 40] 析构:释放了5个int 析构:释放了5个int

完美!两个对象互不干扰,各释放各的内存。


六、三个常见的坑

1. 拷贝构造函数参数不用引用 → 无限递归

cpp

class Bad { public: Bad(Bad other) { // ❌ 传值,会再次调用拷贝构造,无限递归 // ... } };

参数必须用引用,通常是const引用:

cpp

Bad(const Bad& other) { } // ✅

2. 忘了const导致无法拷贝const对象

cpp

class Demo { public: Demo(Demo& other) { } // 参数不是const }; const Demo d1; Demo d2 = d1; // ❌ 错误!不能将const转为非const引用

3. 浅拷贝发生在你没想到的地方

函数传参也会调用拷贝构造函数:

cpp

void func(SafeArray arr) { // 传值,会调用拷贝构造 // ... } SafeArray a(10); func(a); // 这里发生了一次深拷贝

如果数组很大,深拷贝的开销不小。想避免拷贝?用引用传参:

cpp

void func(const SafeArray& arr) { // 不拷贝 // ... }

七、这一篇的收获

你现在应该明白:

  • 拷贝构造函数:用已有对象创建新对象时调用

  • 浅拷贝:默认行为,只复制指针的值,导致两个对象共享内存

  • 深拷贝:自己实现,分配新内存并复制内容,对象各自独立

  • 三法则:需要析构函数 → 就需要拷贝构造和拷贝赋值

💡 小作业:修改上面的SafeArray,故意去掉拷贝构造函数,观察程序会出什么问题。然后加上拷贝构造函数,验证深拷贝解决了问题。


下一篇预告:第5篇《类与对象(四):赋值运算符重载》——=不只是初始化,还有赋值。拷贝构造和赋值运算符有什么区别?什么时候调用哪个?为什么赋值要返回引用?下篇揭晓。

http://www.jsqmd.com/news/794103/

相关文章:

  • Java对接海康威视人脸考勤机实战:Spring Boot整合SDK获取刷卡流水记录
  • G.hn Prime家庭网络技术解析与应用实践
  • LeetCode 最大单词长度乘积题解
  • 从公共卫生演习到社会韧性构建:口罩日的系统设计与实施路径
  • ARM调试架构中DBGCLAIMSET寄存器详解与应用
  • LeetCode 二进制中1的个数题解
  • 终极视频修复指南:使用Untrunc快速恢复损坏的MP4、MOV、M4V文件
  • Obsidian Quiz Generator:用AI从笔记生成交互测验,打造学习闭环
  • 5分钟快速上手:Blender 3MF插件让你轻松实现3D打印模型转换
  • EDA工程师成长与验证技术演进:从算法到芯片的实践闭环
  • AI心智理论评估:VLM意图理解接近人类,但视角采样能力存在瓶颈
  • Edge Impulse实战:TinyML端到端开发平台解析与应用指南
  • 从AMD Ryzen数据误读看硬件市场分析:如何辨别数据信号与噪声
  • SPARQ框架:边缘AI能效优化的三重技术突破
  • LeetCode 汉明距离题解
  • 【AI原生MLOps实战白皮书】:2026奇点大会首发的7大不可复制落地范式,仅限前500位技术决策者获取
  • 物联网标准演进与云平台破局:从M2M到IoT的实战路径
  • 半导体设备再流通:破解成熟制程产能瓶颈与供应链韧性难题
  • 半导体并购新趋势:从规模扩张到价值重构的三大模式解析
  • DevSquad:基于Docker Compose的一站式开发环境解决方案
  • Docker 容器使用指南
  • 栅极后置工艺如何为FDSOI带来颠覆性性能提升?
  • 《Java面试85题图解版(二)》进阶深化中篇:Spring核心 + 数据库进阶
  • 产业公地与紧密设计链:制造业创新效率与供应链韧性的核心
  • turtle学习中的问题
  • 从零部署私有化AI对话框架:igogpt架构解析与实战指南
  • 芯粒技术:从封装协同到UCIe标准,破解芯片设计新范式
  • 从96%本土专利看中国创新转型:成本、策略与全球布局博弈
  • 从CEO到营销技术专家:创业者退休后的身份重构与价值延续
  • 2026 AI技术大会签到暗藏“身份熵阈值”规则,超限即触发人工复核——99.3%参会者不知的3个降熵技巧