item11–在 operator= 中处理“自我赋值

虽然像 w = w 这样的代码看起来很傻,但在使用指针或引用时,“自我赋值”经常以隐蔽的方式发生(例如 a[i] = a[j]i == j 时,或者通过不同的指针指向同一个对象)。

如果你的赋值操作符(Assignment Operator)没有正确处理这种情况,可能会导致资源泄漏数据损坏


1. 问题的根源:先删除,后复制

最典型的错误发生在管理动态资源(如指针)的类中。如果你按照“先释放旧资源,再分配新资源”的逻辑编写代码,就会在自我赋值时崩溃。

错误的示范代码:

class Bitmap { ... };
class Widget {
private:
    Bitmap* pb; // 指向一个堆上的对象
public:
    Widget& operator=(const Widget& rhs) {
        delete pb;                // 1. 错误!如果 rhs 是自己,这里就把自己的资源删了
        pb = new Bitmap(*rhs.pb); // 2. 崩溃!此时 rhs.pb 指向的内存已经被删除了
        return *this;
    }
};

发生了什么? 如果是自我赋值(this&rhs 是同一个地址),第一步 delete pb 不仅删除了当前对象的资源,也删除了 rhs 的资源(因为它们是同一个)。当第二步试图拷贝 rhs.pb 时,实际上是在访问悬空指针(Dangling Pointer)。


2. 解决方案 A:证同测试 (Identity Test)

最直观的方法是在函数开头检查地址是否相同。

Widget& operator=(const Widget& rhs) {
    if (this == &rhs) return *this; // 证同测试:如果是自我赋值,直接返回

    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}
  • 优点: 阻止了自我赋值时的错误,效率高(如果是自我赋值直接跳过)。

  • 缺点: 不具备“异常安全性”(Exception Safety)。如果 new Bitmap 抛出异常(例如内存不足),pb 已经被 delete 了,导致 Widget 对象持有一个被删除的指针。


3. 解决方案 B:精心安排语句顺序 (Ordering Statements)

这是一个更稳健的方法。要在确保新资源已经成功复制之后,才删除旧资源。这天然地处理了自我赋值(因为你先复制了一份自己的副本,再删除旧的,完全没问题)。

Widget& operator=(const Widget& rhs) {
    Bitmap* pOrig = pb;       // 1. 记住原来的指针
    pb = new Bitmap(*rhs.pb); // 2. 尝试复制新资源(如果这里抛出异常,pb 保持原样,安全!)
    delete pOrig;             // 3. 复制成功后,删除旧资源
    
    return *this;
}
  • 优点: 既处理了自我赋值,又具备异常安全性(Exception Safe)。


4. 解决方案 C:Copy and Swap 技术 (最佳实践)

这是现代 C++ 中非常流行的惯用手法(Idiom),它利用了拷贝构造函数交换(Swap)操作。

写法 1(手动拷贝):

Widget& operator=(const Widget& rhs) {
    Widget temp(rhs); // 为 rhs 制作一个副本
    swap(temp);       // 将 *this 的数据与副本交换
    return *this;
} // temp 离开作用域,自动析构,带走了 *this 原来的数据

写法 2(传值拷贝 – 更简洁):

Widget& operator=(Widget rhs) { // 注意这里是传值(By Value),自动调用拷贝构造函数
    swap(rhs); // 将 *this 与 局部副本 rhs 交换
    return *this;
} // rhs 离开作用域,自动析构,带走了 *this 原来的数据
  • 优点:

    1. 代码极其简洁。

    2. 将内存管理的细节转移到了拷贝构造函数和析构函数中,逻辑复用。

    3. 强异常安全保证:如果拷贝失败,异常会在进入函数前(参数构造时)抛出,对象状态不会改变。


总结 (Takeaway)

在编写 operator= 时,必须确保当对象赋值给自己时代码也是正确的:

  1. 确保异常安全(Exception Safety)通常也能自动解决自我赋值安全问题。

  2. Copy-and-Swap 技术是处理这一问题的优雅且常用的解法。

  3. 如果手动管理资源,请记住:先复制新资源,确定成功后,再释放旧资源

发表评论