【c++面向对象编程】第4篇:类与对象(三):拷贝构造函数与深浅拷贝问题
目录一、一个崩溃的程序二、拷贝构造函数是什么调用时机三个场景三、浅拷贝 vs 深拷贝浅拷贝默认行为深拷贝正确的做法四、什么时候必须自己写拷贝构造函数一个反面例子vector的浅拷贝问题五、完整的例子安全的动态数组六、三个常见的坑1. 拷贝构造函数参数不用引用 → 无限递归2. 忘了const导致无法拷贝const对象3. 浅拷贝发生在你没想到的地方七、这一篇的收获一、一个崩溃的程序先看这段代码你觉得它会崩溃吗cppclass 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; } // 程序在这里崩溃运行结果可能正常输出也可能输出乱码最后大概率崩溃。原因很简单s1和s2里面的data指针指向了同一块内存。当程序结束s2先析构delete[]了那块内存然后s1析构再次delete[]同一块内存——重复释放程序崩溃。这就是浅拷贝带来的灾难。二、拷贝构造函数是什么拷贝构造函数是一种特殊的构造函数参数是本类对象的const引用用已有的对象去创建新的对象时自动调用如果你不写编译器会生成一个默认的逐成员复制语法长这样cppclass MyClass { public: // 拷贝构造函数 MyClass(const MyClass other) { // 拷贝逻辑 } };调用时机三个场景cppclass 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) // 只复制指针的值不复制指针指向的内容 {}对于int、double这种值类型浅拷贝没问题。但对于指针复制的是地址不是地址里的内容。浅拷贝的问题两个对象指向同一块内存一个修改另一个也跟着变可能不是你想要的效果一个释放另一个变成悬空指针重复释放导致崩溃深拷贝正确的做法深拷贝的做法不复制指针的值而是复制指针指向的内容。cppclass 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; } // 后面会讲赋值运算符重载 };现在运行之前会崩溃的例子cppint 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如果类需要自定义析构函数那么它几乎一定也需要自定义拷贝构造函数和拷贝赋值运算符。具体来说以下情况必须写拷贝构造函数类里有指针成员并且构造函数里用new分配了内存类里有文件句柄、数据库连接等需要“独占”的资源类里有互斥锁mutex两个对象拥有同一个锁会导致死锁一句话默认的逐成员复制对你的资源管理方式不适用时。一个反面例子vector的浅拷贝问题cppclass IntVector { private: int* arr; int size; public: IntVector(int n) : size(n) { arr new int[n]; for(int i0; in; 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); // 输出999v1被意外修改了 // 程序结束两次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. 拷贝构造函数参数不用引用 → 无限递归cppclass Bad { public: Bad(Bad other) { // ❌ 传值会再次调用拷贝构造无限递归 // ... } };参数必须用引用通常是const引用cppBad(const Bad other) { } // ✅2. 忘了const导致无法拷贝const对象cppclass Demo { public: Demo(Demo other) { } // 参数不是const }; const Demo d1; Demo d2 d1; // ❌ 错误不能将const转为非const引用3. 浅拷贝发生在你没想到的地方函数传参也会调用拷贝构造函数cppvoid func(SafeArray arr) { // 传值会调用拷贝构造 // ... } SafeArray a(10); func(a); // 这里发生了一次深拷贝如果数组很大深拷贝的开销不小。想避免拷贝用引用传参cppvoid func(const SafeArray arr) { // 不拷贝 // ... }七、这一篇的收获你现在应该明白拷贝构造函数用已有对象创建新对象时调用浅拷贝默认行为只复制指针的值导致两个对象共享内存深拷贝自己实现分配新内存并复制内容对象各自独立三法则需要析构函数 → 就需要拷贝构造和拷贝赋值 小作业修改上面的SafeArray故意去掉拷贝构造函数观察程序会出什么问题。然后加上拷贝构造函数验证深拷贝解决了问题。下一篇预告第5篇《类与对象四赋值运算符重载》——不只是初始化还有赋值。拷贝构造和赋值运算符有什么区别什么时候调用哪个为什么赋值要返回引用下篇揭晓。