C++含指针成员字段的类如何正确实现拷贝构造函数
实现方向正确,但当前代码存在明确的边界漏洞和实践偏差
先说你做对的部分
你没有直接复制原对象的指针地址、而是为指针成员申请独立堆内存、拷贝指向值的思路,完全符合深拷贝的核心要求,从根本上避免了浅拷贝导致的两个对象共用同一块内存、修改互相影响、析构时重复释放同一块内存的问题,这个方向没有错。
你从C#开发转过来的直觉其实是适配的:C#里由GC托管的引用类型默认是引用传递,但C++里裸指针的默认行为就是纯地址拷贝,没有GC帮你托管内存所有权,你手动做的独立内存分配动作,本质上就是在模拟你熟悉的“值独立副本”的效果。
当前代码存在的具体问题
- 最容易触发未定义行为的漏洞:当
source.agePtr为nullptr时,你没有把当前正在构造的对象的agePtr成员显式置空。C++的类成员如果在构造函数体里不主动初始化,根本不会自动置空,会直接保留栈上的随机垃圾值成为野指针。后续不管是析构时执行delete agePtr、还是判断指针有效性、还是给指针赋值,全都会触发崩溃、内存篡改这类随机问题。 - 违反C的三/五法则:只要你的类需要手动为指针成员申请堆内存,就不能只单独写拷贝构造函数。必须配套实现析构函数、拷贝赋值运算符,C11及之后的版本还要补移动构造、移动赋值的逻辑,不然漏洞根本堵不上:
- 不写析构函数的话,你
new出来的内存永远不会被释放,会一直内存泄漏 - 不写拷贝赋值运算符的话,编译器会自动生成默认的浅拷贝逻辑,遇到
person2 = person1这类赋值操作时,还是会直接复制指针地址,你在拷贝构造里写的深拷贝逻辑完全失效
- 不写析构函数的话,你
- 额外的实践偏差:你现在用裸指针手动申请堆内存存一个
int的写法,本身就不是现代C++推荐的做法,很容易写出漏delete、重复delete、野指针的bug,普通业务代码完全没必要这么折腾。
修正后的最小可运行正确实现
class Person { // 类内初始值兜底,哪怕后续构造漏写,成员也默认是空指针,不会成野指针 private: int* agePtr = nullptr; public: Person(int age) : agePtr(new int(age)) {} // 修正边界的拷贝构造:不管源对象指针是不是空,都给当前成员明确赋值 Person(const Person& source) { agePtr = source.agePtr ? new int(*source.agePtr) : nullptr; } // 配套实现析构,释放申请的堆内存 ~Person() { delete agePtr; } // 配套实现拷贝赋值,避免浅拷贝问题 Person& operator=(const Person& other) { if (this == &other) return *this; // 先释放自身原有内存,再重新分配拷贝 delete agePtr; agePtr = other.agePtr ? new int(*other.agePtr) : nullptr; return *this; } void ChangePtr(int newAge) { *agePtr = newAge; } int* GetAgePtr() const { return agePtr; } };
你写的验证逻辑本身是对的:打印两个对象的指针地址、修改第一个对象的指向值之后检查第二个对象的内容,就能验证深拷贝是不是生效。用上面这个修正后的实现跑你的验证代码,就能看到预期效果:修改person1的年龄值之后,person2的指针指向的内容不会跟着变,两个对象的指针地址也完全独立。
更符合现代C++最佳实践的简化方案
绝大多数普通业务场景下,你根本没必要用指针成员存一个整数:直接把成员改成普通栈上的int age,编译器自动生成的拷贝构造、赋值、析构逻辑完全正确,零出错概率。
如果确实需要动态内存,也优先用std::unique_ptr、std::shared_ptr这类标准库智能指针,把内存释放、空值判断的逻辑交给标准库实现,根本不用自己写手动内存管理的代码。只有在自实现数据结构、极致内存布局优化这类特殊场景下,才需要写你现在这种手动管理裸指针内存的逻辑。
内容的提问来源于stack exchange,提问作者DarkAtomz
相关产品推荐
相关产品推荐

