在 C++ 中,拷贝构造函数 (Copy Constructor)是一种特殊的构造函数,用于用一个已存在的对象初始化一个新的同类对象。它的调用时机和深拷贝实现密切相关,尤其当类中包含指针成员 或动态资源时。
一、拷贝构造函数的调用时机
根据权威资料(如 [3][7][8]),拷贝构造函数在以下 三种情况 下会被自动调用:
-
用一个对象初始化另一个对象
cppMonster m1(5, "poison", {10, 20}); Monster m2(m1); // 调用拷贝构造函数 Monster m3 = m1; // 等价于上一行,也调用拷贝构造函数,相当于自动隐式转换成显式的M m3 = M(m1) -
对象以值传递方式作为函数参数
cppvoid func(Monster m); // 参数按值传递 func(m1); // 调用拷贝构造函数 -
函数以值方式返回一个局部对象
cppMonster createMonster() { Monster m(3, "burn", {5}); return m; // 返回时调用拷贝构造函数(可能被 RVO 优化,但语义上会调用) }
⚠️ 注意:如果未显式定义拷贝构造函数,编译器会生成默认的浅拷贝版本 ,对
char* effect这类指针成员仅复制地址,导致多个对象共享同一块内存,析构时可能重复释放,引发未定义行为。
二、Monster 类的深拷贝实现
假设类定义如下:
cpp
class Monster {
public:
int level;
char* effect; // 指向动态分配的字符串
std::vector<int> atks; // 标准库容器,自带深拷贝语义
};
✅ 正确实现深拷贝需重写:
- 拷贝构造函数
- 拷贝赋值运算符
- 析构函数
这称为 "三法则"(Rule of Three)。
完整示例代码:
cpp
#include <iostream>
#include <vector>
#include <cstring>
class Monster {
public:
int level;
char* effect;
std::vector<int> atks;
// 构造函数
Monster(int l, const char* eff, const std::vector<int>& a)
: level(l), atks(a) {
if (eff) {
effect = new char[std::strlen(eff) + 1];
std::strcpy(effect, eff);
} else {
effect = nullptr;
}
}
// 拷贝构造函数(深拷贝)
Monster(const Monster& other)
: level(other.level), atks(other.atks) {
if (other.effect) {
effect = new char[std::strlen(other.effect) + 1];
std::strcpy(effect, other.effect);
} else {
effect = nullptr;
}
std::cout << "深拷贝构造函数被调用\n";
}
// 拷贝赋值运算符(深拷贝 + 自赋值安全)
Monster& operator=(const Monster& other) {
if (this != &other) { // 防止自赋值
delete[] effect; // 释放原资源
level = other.level;
atks = other.atks; // vector 自动深拷贝
if (other.effect) {
effect = new char[std::strlen(other.effect) + 1];
std::strcpy(effect, other.effect);
} else {
effect = nullptr;
}
}
return *this;
}
// 析构函数
~Monster() {
delete[] effect;
effect = nullptr;
}
};
三、深拷贝注意事项(关键点)
| 问题 | 说明 |
|---|---|
| 1. 指针成员必须手动深拷贝 | char* effect 需 new 新内存并 strcpy 内容,不能只复制指针 |
2. std::vector<int> 不需要特殊处理 |
标准库容器自带正确的拷贝语义,会自动深拷贝其内部元素 |
| 3. 必须处理自赋值 | 在 operator= 中检查 if (this != &other),否则 delete[] 后无法再读取 other.effect |
| 4. 先释放旧资源再分配新资源 | 避免内存泄漏 |
| 5. 遵循"三法则" | 有指针成员 → 必须同时定义 析构函数、拷贝构造、拷贝赋值 |
| 6. 异常安全建议 | 更健壮的做法是使用 copy-and-swap 技术,先构造临时副本再交换,保证强异常安全 |
四、现代 C++ 建议(最佳实践)
为避免手动管理内存,优先使用 RAII 类型:
cpp
class Monster {
public:
int level;
std::string effect; // 替代 char*
std::vector<int> atks; // 已很好
// 无需手写拷贝构造/赋值/析构!编译器生成的即可正确深拷贝
};
✅ 使用
std::string和std::vector后,默认的拷贝行为就是深拷贝 ,且安全高效,符合 "零法则"(Rule of Zero)。
总结
- 拷贝构造函数在初始化、传参、返回值时调用;
Monster中因含char*,必须手动实现深拷贝;vector<int>成员无需额外处理;- 最佳方案:用
std::string替代裸指针,避免手写拷贝逻辑。