Effective C++条款11:在operator=中处理“自我赋值”

一、自我赋值的演示案例

  • 自我赋值会发生在很多情况下
  • 例如定义一个类对象,将自己赋值给自己
 
  1. class Widget{};

  2.  
  3. Widget w;

  4. w = w; //自我赋值

  • 如果下面的a是一个数字,且索引i和j相等,那么也是一个潜在的自我赋值
a[i] = a[j];
  • 例如下面两个指针px和py指向于同一块内存,那么也是一个潜在的自我赋值
*px = *py;

二、一个自我赋值产生的错误

  • 下面建立一个类Widget,其中有一个指针指向一块动态分配的位图
 
  1. class Bitmap {};

  2. class Widget {

  3. public:

  4. //赋值运算符

  5. Widget& operator=(const Widget& rhs)

  6. {

  7. delete pb;

  8. pb = new Bitmap;

  9. return *this;

  10. }

  11. private:

  12. Bitmap* pb;

  13. };

  • 现在来分析这个运算符如果出现自我赋值而产生的错误:
    • 如果参数rhs传入的就是自身,那么当pb被释放之后,下面再次new的时候又将参数(自己)的pb指针所指的内容传入进去,但是pb的内容已经被释放了,因此再次使用到这个对象的时候就会产生不确定的行为
 
  1. Widget& operator=(const Widget& rhs)

  2. {

  3. delete pb; //释放当前对象的pb

  4. pb = new Bitmap(*rhs.pb); //以参数为副本调用拷贝构造函数重新创建

  5. return *this;//返回自身

  6. }

三、在赋值运算符中检测释放为“自我赋值”

  • 根据上面“自我赋值”而产生的错误,我们应该在赋值运算符函数的第一步判断传入的对象是否为自己,如果为自己的话做相应的处理
 
  1. //参数的&为引用

  2. Widget& operator=(const Widget& rhs)

  3. {

  4. //判断是否为“自我赋值”

  5. //注意,此处的&为取地址

  6. if (this == &rhs)

  7. return *this;

  8.  
  9. //其他的与上面介绍的一样

  10. delete pb;

  11. pb = new Bitmap(*rhs.pb); //以参数为副本调用拷贝构造函数重新创建

  12. return *this;

  13. }

四、异常处理

  • 上面介绍的operator=虽然解决了“自我赋值”检测,但是不是“异常安全的”。例如在new操作符执行时跑出了异常(内存不足或因为Bitmap类的拷贝构造函数抛出异常),最终Widget对象会只有一个一块已被删除的Bitmap,因此代码是不安全的
  • 我们需要自己设计代码来处理异常,例如我们修改“三”中的代码:
    • 此段代码可以来处理异常:如果new时抛出了异常,此时我们的pb对象还没有删除
    • 此段代码还可以来处理“自我赋值”:我们对原bitmap做了一份复制、删除原bitmap,然后将pb再指向于复制的那一份。这个虽然不是处理“自我赋值”最高效的办法,但是行得通
 
  1. Widget& operator=(const Widget& rhs)

  2. {

  3. Bitmap *pOrig = pb; //记住原先的pb

  4. pb = new Bitmap(*rhs.pb); //以参数为副本让pb重新创建一个对象

  5. delete pOrig; //删除原先的pb

  6. return *this;

  7. }

  • 此处为什么我们不在代码最前面进行“对象是否为自己”的检测了:此处我们的代码已经可以处理自我赋值了,如果还添加“三”中那种“自我检测”的代码,会使代码增多并多了一个语句判断,会使执行速度降低

五、使用“copy and swap”技术来处理自我赋值

  • 替换上面的所有办法,我们可以使用“copy and swap”技术来解决“自我赋值”以及“异常处理”
  • copy and swap技术和“异常安全性”有密切关系,会在条款29详细讲述

实现手法①

  • 现在我们修改代码,最终的代码如下所示:
    • 现在这段代码既可以处理异常安全,也可以处理自我赋值
 
  1. class Bitmap {};

  2. class Widget {

  3. public:

  4. void swap(Widget& rhs); //将参数rhs与*this进行数据交换

  5. Widget& operator=(const Widget& rhs)

  6. {

  7. Widget temp(rhs);//以函数参数为参数调用Wiget的拷贝构造函数创建一个对象

  8. //不能将rhs直接传入swap,因为这样的话会改变=号后面对象的内容,因此上面需要创建一个临时对象temp

  9. swap(temp); //交换参数所指的对象与*this

  10. return *this;

  11. }

  12. private:

  13. Bitmap* pb;

  14. };

实现手法②

  • 实现这种技术的手法还有一种是以“传值调用”operator=,实现如下:
    • 这种技术手法实现的功能与上面的一样,但是代码没有那么清晰
    • 但是这种手法将“拷贝”动作从函数体内移动到了“函数参数构造阶段”,因此效率提高了
 
  1. class Bitmap {};

  2. class Widget {

  3. public:

  4. void swap(Widget& rhs); //将参数rhs与*this进行数据交换

  5. //rhs是被传对象的一份副本,这样的话我们就不用在operator=为参数创建一个临时对象了

  6. Widget& operator=(Widget rhs)

  7. {

  8. swap(rhs); //将传入的副本与*this进行数据替换

  9. return *this;

  10. }

  11. private:

  12. Bitmap* pb;

  13. };

六、总结

  • 确保当前对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy and swap
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值