前言
我们在重载operator=时需要注意自我赋值的情况。可能有人会认为这种情况很少发生,因为很少有人会写诸如w=w的句子。但实际上,代码中出现a[i]=a[j](i==j),或者p=q(指向同一对象)的例子屡见不鲜。
事实上,只要你使用了pointers或者references,而且它们被用来指向多个相同类型的对象,我们就需要考虑 是否会出现自赋值的情况。确切地说,只要有多个对象来自同一个继承体系,比如一个指针指向base class类型,而另一个指向derived class类型。它们完全可能指向的是同一个对象。
不处理自赋值情况的风险
如果我们在class中手动地进行了资源管理,那么自赋值的处理不当可能会带来一些问题。举例而言:1
2
3
4
5
6
7
8
9
10
11
12class Bitmap{...}
class Widget{
...
private:
Bitmap* pb;
};
Widget&
Widget::operator=(const Widget& rhs){
delete pb;
pb = new Bitmap(*rhs.pb);//如果是自赋值,pb指向的对象已经释放
return *this;
}
上述程序既不具备异常安全性,也直接导致了“在停止使用资源前意外地释放了它”。
解决方案
证同测试
其原理很简单:在函数最前端放上这样一句话:1
if(this==&rhs) return *this;
这个解决方案保证了自赋值安全性,但是不具备异常安全性。在这句话中:1
pb=new Bitmap(*rhs.pb);//已通过证同测试,此时已经执行delete
如果new导致了一个异常(内存不足或者copy构造函数抛出异常),那么pb始终会指向一块被删除的Bitmap。我们无法安全地删除pb,甚至读取它。
手动保证异常安全
因为只要解决了异常安全性,那么自赋值安全性就会必然得到解决,所以我们倾向于使用足够完美的逻辑避免异常的发生,其原理也很简单:在赋值pb前别删pb1
2
3
4bitnap * porig = pb;
pb =new bitmap(rhs.pb);
delete porig;
return *this;
这样的好处不言自明,如果pb没能正确赋值,其对象也没有被析构。(异常发生时跳出了operator=函数,因此没有执行delete porig)。如果正确赋值,pb指向的对象被释放。
copy&&swap
还有一个十分灵巧的operator=撰写技巧:1
2
3
4
5
6
7
8
9
10class Widget{
...
void swap(Widegt& rhs);//彻底交换数据
}
Widget&
Widget::operator=(const Widget& rhs){
Widget temp(rhs);
swap(temp);
return *this;
}
如果operator=接受的参数是by value而非by reference,那我们连temp都可以省略:
1
2
3
4
5
Widget&
Widget::operator=(Widget rhs){
swap(rhs);
return *this;
}
1 | Widget& |
总结
- 确保自赋值安全性。其技术包括比较lhs与rhs的地址,先做backup,以及copy-and-swap
- 确定任何操作一个以上的对象的函数,如果其操作对象是同一个对象时,还能保持正确性。