条款11:在**重点内容**operator=中处理“自我赋值”
“自我赋值”发生在对象被赋值给自己时:
class Widget{…}
Widget w;
…
w=w;//赋值给自己
a[i]=a[j];//潜在的自我赋值,如果i和j有相同的值,这便是个自我赋值
*px=*py;//潜在的自我赋值,如果px和py指向同一个问题,这也是自我赋值
甚至如下这样也可以:
class Base {…};
class Derived:public Base {…};
void doSomething(const Base& rb,Derived* pd);//rb和pd可能其实是同一个对象
很多情况下“自我赋值”是安全的,不需要额外操心,然而如果你尝试自行管理资源(如果你打算写一个用于资源管理的class就这样做),可能会掉进“在停止使用资源之前以外释放了它”的陷阱,如:
class Bitmap {...};
class Widget{
...
private:
Bitmap* pb;
};
Widget& Widget::operator=(const Widget& rhs)
{
delete pb;
pb=new Bitmap(*rhs.pb);
return *this;
}
这里的自我赋值问题是,operator=函数内部*this和rhs有可能是同一个对象,如果这样的话,delete pb销毁的不只是当前对象那个的bitmap,同时rhs的bitmap也被销毁掉了,那么在自我赋值的时候程序会发现rhs.pb指针指向的是一个已经被删除的对象!
如何来阻止这种错误呢,具体有多种方法:
1)传统做法是operator=最前面加一个“证同测试”达到“自我赋值”的校验目的,具体见如下代码:
Widget& Widget::operator=(const Widget& rhs){
if(this==&rhs) return *this;//证同测试
delete pb;
pb=new Bitmap(*rhs.pb);
return *this;
}
这样的代码有没有什么错误呢?可以看到,具备了“自我赋值安全性”,那还有什么问题呢?答案就是“异常安全性”,这种解决方法对于异常并没有什么很好的作用,具体来说,如果new Bitmap(*rhs.pb)出现异常的话(不论是因为分配时内存不足或者Bitmap的copy构造函数抛出异常),pb最终指向一个被删除的Bitmap,这样的指针会出现空指针异常等乙烯类问题。
2)这种方法主要侧重于“异常安全性”方面,我们只需注意在赋值pb所指的东西之前别删除pb:
Widget& Widget::operator=(const Widget& rhs){
Bitmap* pOrig=pb;
pb=new Bitmap(*rhs.pb);
delete pOrig;//删除原先的pb
return *thsis;
}
这样的话,如果“new Bitmap”抛出异常,pb保持原状,即使没有“证同测试”,这段代码还是可以处理自我赋值,因为我们对原bitmap做了一份复件、删除原bitmap、然后指向新制造的那个复件。PS:如果此时需要考虑效率的话,需要先估计“自我赋值”发生的频率有多高?如果高的话,我们就将“证同测试”加在函数起始处?如果“证同测试”发生频率不高的话,推荐不要添加“证同测试”,因为添加这项测试也需要耗费成本,降低程序执行效率。
3)如果对上述两种方法还是不太满意的话,还有一个替代方案,不仅“异常安全”同时“自我赋值安全”,即使用copy and swap技术实现,参看以下代码:
class Widget{
...
void swap(Widget& rhs);
...
};
Widget& Widget::operator=(const Widget& rhs){
Widget temp(rhs);//使用copy构造函数,为rhs数据制作一份副本
swap(temp); //将*this数据和上述所述复件的数据交换
return *this;
}
怎样,这种方法不错吧!但是代码看起来逻辑并没有多么清晰,这也是这种方法的缺点吧!
总结:
1)确保当前对象自我复制时operator=有良好的行为,其中包括“来源对象”和“目标对象”的地址、精心周到的语句顺序以及copy-and-swap操作;
2)确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象那个时候,其行为依然正确。