Effective C++ 条款11:在operator=中处理“自我赋值”
核心思想:确保拷贝赋值操作符(operator=
)在对象给自己赋值时(如 a = a;
)能安全运行,避免资源泄漏、指针悬挂等问题。自我赋值可能通过别名、引用或指针间接发生,需显式处理。
⚠️ 1. 自我赋值的隐藏风险
以下代码存在致命风险:
class Bitmap { /*...*/ };
class Widget {
Bitmap* pb; // 指向堆内存的指针
public:
Widget& operator=(const Widget& rhs) {
delete pb; // 释放当前资源
pb = new Bitmap(*rhs.pb); // 复制rhs的资源
return *this;
}
};
问题:
当 rhs
和 this
是同一对象时(*this = *this;
):
delete pb
销毁当前对象的资源new Bitmap(*rhs.pb)
访问已被销毁的pb
→ 未定义行为
🛡️ 2. 解决方案
✅ 方法1:证同测试(Identity Test)
Widget& operator=(const Widget& rhs) {
if (this == &rhs) return *this; // 关键:检查是否自我赋值
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
优点:阻止自我赋值的破坏性操作
缺点:若 new Bitmap
抛出异常,pb
指向已删除内存 → 异常不安全
✅ 方法2:精心安排语句顺序
Widget& operator=(const Widget& rhs) {
Bitmap* pOrig = pb; // 记住原指针
pb = new Bitmap(*rhs.pb); // 先复制新资源
delete pOrig; // 再释放旧资源
return *this;
}
优点:
- 自我赋值安全:复制新资源后再删除旧资源
- 异常安全:若
new
抛出异常,pb
保持原状
✅ 方法3:Copy and Swap(推荐)
class Widget {
void swap(Widget& rhs); // 交换*this和rhs的所有成员
public:
Widget& operator=(const Widget& rhs) {
Widget temp(rhs); // 创建副本(调用拷贝构造函数)
swap(temp); // 交换*this和副本
return *this; // 副本离开作用域自动销毁旧资源
}
};
优化版(利用值传递):
Widget& operator=(Widget rhs) { // 值传递:自动调用拷贝构造
swap(rhs); // 交换*this和副本
return *this; // 副本销毁旧资源
}
优势:
- 代码简洁
- 天然处理自我赋值和异常安全
💡 关键原则总结
方案 | 自我赋值安全 | 异常安全 | 代码简洁性 |
---|---|---|---|
证同测试 | ✅ | ❌ | ★★☆ |
语句顺序调整 | ✅ | ✅ | ★★★ |
Copy and Swap | ✅ | ✅ | ★★★★ |
- 自我赋值的隐蔽性
- 可能通过别名(
a = b;
且a
和b
指向同一对象)或链式操作(arr[i] = arr[j];
)发生。
- 可能通过别名(
- 异常安全的重要性
- 若资源分配失败(如
new
抛出异常),对象必须保持有效状态。
- 若资源分配失败(如
- 终极解决方案:Copy and Swap
- 结合拷贝构造函数和
swap
,确保强异常安全和自我赋值安全。
- 结合拷贝构造函数和
示例代码:Copy and Swap 实现
class Widget { Bitmap* pb; void swap(Widget& rhs) noexcept { using std::swap; swap(pb, rhs.pb); } public: Widget& operator=(Widget rhs) { // 按值传递 swap(rhs); return *this; } };