Effective C++ 条款12:复制对象时勿忘其每一个成分
Effective C++ 条款12:复制对象时勿忘其每一个成分
核心思想:自定义拷贝构造函数和拷贝赋值操作符时,必须确保复制对象的所有成员变量,包括基类成分。编译器不会检查用户自定义拷贝函数中的遗漏,任何缺失都可能导致难以察觉的运行时错误。
⚠️ 1. 编译器默认拷贝函数的局限性
- 编译器生成的拷贝函数会自动复制所有成员变量(内置类型逐位拷贝,自定义类型调用其拷贝函数)。
- 但若用户自定义拷贝函数,编译器不再生成默认版本,且不会提醒复制所有成员。
示例:成员变量遗漏
class Customer {std::string name;int loyaltyPoints;
public:Customer(const Customer& rhs) : name(rhs.name) {} // 遗漏loyaltyPoints!Customer& operator=(const Customer& rhs) {name = rhs.name; // 遗漏loyaltyPoints!return *this;}
};
后果:
- 拷贝后新对象的
loyaltyPoints
值随机(栈内存残留值)→ 严重BUG!
🚨 2. 继承中的基类成分遗漏
派生类自定义拷贝函数时,必须显式调用基类拷贝函数:
class PriorityCustomer : public Customer {int priorityLevel;
public:PriorityCustomer(const PriorityCustomer& rhs): Customer(rhs), // 关键:调用基类拷贝构造priorityLevel(rhs.priorityLevel) {}PriorityCustomer& operator=(const PriorityCustomer& rhs) {Customer::operator=(rhs); // 关键:调用基类拷贝赋值priorityLevel = rhs.priorityLevel;return *this;}
};
错误做法:
// 派生类拷贝构造函数
PriorityCustomer(const PriorityCustomer& rhs): priorityLevel(rhs.priorityLevel) {}
后果:
基类成员变量(如 name
, loyaltyPoints
)未被复制 → 基类部分数据丢失!
⚖️ 3. 拷贝构造与拷贝赋值的交互原则
操作 | 交互原则 |
---|---|
拷贝构造函数 | 可调用拷贝赋值操作符? ❌ 语法错误(对象尚未构造完成) |
拷贝赋值操作符 | 可调用拷贝构造函数? ❌ 会创建新对象而非修改当前对象 |
黄金法则:
- 消除代码重复:将共享逻辑提取为私有工具函数(如
init()
或copyFrom()
)。- 禁止互相调用:拷贝构造和拷贝赋值是不同操作,需独立实现核心逻辑。
正确实践:
class Customer {
private:void copyFrom(const Customer& rhs) { // 共享的复制逻辑name = rhs.name;loyaltyPoints = rhs.loyaltyPoints;}
public:Customer(const Customer& rhs) { copyFrom(rhs); }Customer& operator=(const Customer& rhs) {copyFrom(rhs);return *this;}
};
💡 关键原则总结
- 复制所有成员变量
- 检查拷贝构造函数的初始化列表和拷贝赋值操作符的函数体,确保每个成员变量都被复制。
- 显式处理基类成分
- 派生类拷贝构造函数:初始化列表中调用基类拷贝构造(
: Base(rhs)
)。 - 派生类拷贝赋值操作符:函数体内调用基类拷贝赋值(
Base::operator=(rhs);
)。
- 派生类拷贝构造函数:初始化列表中调用基类拷贝构造(
- 避免拷贝函数互相调用
- 用第三方函数(如
copyFrom()
)封装共享逻辑,保持代码DRY(Don’t Repeat Yourself)。
- 用第三方函数(如
错误示例诊断:基类成分缺失的后果
PriorityCustomer a; PriorityCustomer b(a); // b的Customer部分(如name)未被复制 → 与a的基类数据不一致!