Effective C++ Item 11: 在operator=中处理自赋值问题

一、定义赋值运算符函数需要注意的问题:

  1. 返回值类型声明为引用,并在函数返回前返回*this。因为只有返回引用,才能允许连续赋值。
  2. 传入参数应声明为常量引用,否则从形参到实参会多调用一次拷贝构造函数,降低代码效率。赋值运算符函数不会改变传入的实例的状态,因此传入参数应添加const关键字。
  3. 是否释放实例自身已有的内存。如果忘记分配新内存之前,释放自身已有的空间,程序将出现内存泄露。
  4. 是否判断传入的参数和this是不是同一个实例。如果是同一个实例,则直接返回this。如果事先不判断就进行赋值,那么在释放实例自身的内存的时候就会导致严重的问题:当*this和传入的参数是同一个实例时,那么一旦释放了自身的内存,传入的参数的内存也同时被释放了,因此再也找不到需要赋值的内容了。

from 剑指offer

二、什么是自我赋值?

“自我赋值”发生在对象被赋值给自己时:

class Widget { ... };

Widget w;
...
w = w;          //赋值给自己

虽然这种做法看起来比较傻,但是这种操作却是合法的。
此外,自我赋值并不是总是可以一眼分辨出来,例如:

  1. 如果i和j具有相同的值时,这就是一个自我赋值。
a[i] = a[j];        //潜在的自我赋值
  1. 如果指针px和py恰巧指向同一个东西,这也是一个自我赋值。
*px = *py;          //潜在的自我赋值

这些并不明显的复制行为,是“别名(aliasing)”所带来的结果。 比如,当基类指针和派生类指针指向同一个基类的对象时,自我赋值就可能发生。

三、不安全的做法

举个例子,假如建立一个class用来保存一个指针指向一块动态分配的位图(bitmap):

class Bitmap { ... };
class Widget {
    ...
private:
    Bitmap* pb;     //指针,指向一个从heap分配而得的对象
};

接着,下面的operator= 的实现代码,看起来虽然合理,但是在进行自我赋值时并不安全:

Widget& Widget::operator=(const Widget& rhs)    //不安全的operator= 的实现版本
{
    delete pb;     //停止使用当前的bitmap
    pb = new Bitmap(*rhs.pb);       //使用rhs's bitmap的拷贝
    return *this;
}

*this和rhs可能是相同的对象,这样的话delete不仅删除this指针的bitmap,也删除了rhs的bitmap。

问题:不仅自赋值不安全,异常处理也不安全,因为new Bitmap操作是可能产生异常的。

四、解决方法

1. 传统做法——Identity test, 自赋值检测

Widget& Widget::operator=(const Widget& rhs)

{

  if (this == &rhs) return *this;   // identity test: if a self-assignment,

                                    // do nothing

  delete pb;

  pb = new Bitmap(*rhs.pb);



  return *this;

}

缺点:new Bitmap的异常安全问题仍存在。

2. 复制pb所指东西之前不要删除pb

实现异常安全通常也能实现自赋值安全。

Widget& Widget::operator=(const Widget& rhs)

{

  Bitmap *pOrig = pb;               // remember original pb

  pb = new Bitmap(*rhs.pb);         // make pb point to a copy of *pb

  delete pOrig;                     // delete the original pb



  return *this;

}

  • 现在如果new Bitmap抛出异常,pb保持不变。即使不做identity test,它也能处理好自赋值问题。
  • 相对来说,这种方法的效率较低。如果考虑效率问题,可以函数顶部添加自赋值检测代码。但这样做之前,需要考虑自赋值操作发生的频率可能有多高,因为这样做会有一定开销,因为它会导致代码增加以及引入了流程控制分支,会降低运行速度。比如,指令预取、缓存和流水线的效率可能被降低。

3. copy and swap

class Widget {

  ...

  void swap(Widget& rhs);   // exchange *this's and rhs's data;

  ...                       // see Item 29 for details

};



Widget& Widget::operator=(const Widget& rhs)

{

  Widget temp(rhs);             // make a copy of rhs's data



  swap(temp);                   // swap *this's data with the copy's

  return *this;

}

五、如果类成员含有的unique_ptr指针,而没有raw pointer,那么在它的赋值运算符函数中不处理自赋值问题会有什么问题吗?

如果类成员只有std::unique_ptr,而没有裸指针,那么在赋值运算符函数中不处理自赋值问题是没有问题的。

原因如下:

std::unique_ptr的赋值操作会自动处理自赋值问题。当一个std::unique_ptr被赋值时,原有的内存将被自动删除,然后才会接管新的内存。

举例来说,假设有以下的代码:

class Foo {
public:
    std::unique_ptr<int> ptr;

    Foo& operator=(const Foo& other) {
        if (this != &other) {
            ptr = std::make_unique<int>(*other.ptr);
        }
        return *this;
    }
}; 

即使不进行自赋值检查,当进行自赋值时,std::unique_ptr的赋值操作也不会导致问题。这是因为std::unique_ptr的赋值操作会首先删除当前的内存,然后再分配新的内存。因此,即使是自赋值,也不会有问题。

然而,这不意味着在所有的情况下都可以忽略自赋值检查。如果类成员是裸指针或者其他需要手动管理内存的资源,那么就必须在赋值运算符函数中处理自赋值问题,以防止内存泄露或者其他问题。

posted on 2023-02-06 15:02  七昂的技术之旅  阅读(64)  评论(0编辑  收藏  举报

导航