所谓的“自我赋值”发生在对象被赋值给自己时:
class Object { /* .. */ }
int main()
{
Object o;
o = o; //自我赋值。
return 0;
}
这种写法看上去没有任何意义,但是它是可以通过编译的;最关键的是,这种操作往往不会显示地发生,例如:
int main()
{
Object o;
//...
Object *p1 = &o;
Object *p2 = &o;
//...
*p1 = *p2
return 0;
}
在上面这种情况(及其所有类似的情况)中,如果两个指针或引用指向同一个对象,那么这种赋值操作同样是属于自我赋值;更有甚者,两个对象只要是来自于同一个继承体系,那么即便它们的类型不同,这两个对象也很有可能压根就是一个(即它们拥有相同的地址):
class Base { /* ... */ };
class Derived : public Base { /* ... */ };
//这里的rb和pd可能指向的是同一个对象。
void function(const Base &rb, Derived *pd);
如果遵循RTTI
的忠告,那么我们会使用对象来管理资源,而且可以确定所谓的“资源管理对象”在拷贝发生时会做出正确的举措;然而如果我们尝试自己管理资源(例如,我们正在编写资源管理类),就可能会掉进“在停止使用资源之前就意外地将其释放”这样的错误陷阱当中。
例如,我们建立了一个类来表示“位图”:
class BitMap { /* ... */ };
class Widget
{
BitMap *bitMap;
public:
//...
Widget &operator=(const Widget &other);
};
下面是一份operator=
的实现代码,表面上看起来很合理,但是在自我赋值的时候并不安全:
Widget &Widget::operator=(const Widget &other)
{
delete bitMap;
bitMap = new BitMap(*(other.bitMap));
return *this;
}
这里的赋值问题是,operator=
函数内的*this
和other
很有可能是同一个对象。一旦这种情况发生,那么delete bitMap
操作就销毁了这个函数中唯一一个可用的BitMap
对象,因而这个对象将不再持有合法的BitMap
对象指针。
解决这个问题的方法是在一开始就判断*this
和other
是否是同一个对象:可以通过检查它们的地址是否相同来判断:
Widget &Widget::operator=(const Widget &other)
{
if(this != &other) {
delete bitMap;
bitMap = new BitMap(*(other.bitMap));
}
return *this;
}
这样做行得通。
现在来讨论另一个问题:异常安全性
。这个新的实现方案仍然存在“异常安全”的麻烦:现在假设new BitMap
操作失败了,那么从今往后当前对象将会持有一个非法的BitMap
对象指针。
所谓异常安全性是指以下两个方面:
- 不泄露资源。
- 不破坏数据。
幸运的是,很多时候一组精心安排的代码可以满足异常安全性
的要求。例如:
Widget &Widget::operator=(const Widget &other)
{
if(this != &other) {
BitMap *pTmp = bitMap;
bitMap = new BitMap(*(other.bitMap));
delete pTmp;
}
return *this;
}
现在,如果new BitMap
抛出了异常,那么原对象将不会出现问题。
还要介绍一种非常精妙的方案,它可以实现operator=
的自我赋值安全性以及异常安全性,并且实现方案非常直观:
class BitMap { /* ... */ };
class Widget
{
BitMap *bitMap;
public:
void swap(Widget &other)
{
BitMap *pTemp = bitMap;
bitMap = other.bitMap;
other.bitMap = pTemp;
//...
}
//...
Widget &operator=(const Widget &other)
{
Widget tmp(other);
swap(tmp);
return *this;
}
};
通过构造一个other
对象的副本,然后在将当前对象和这个副本进行数据交换的方式,可以完美地避开上述两个问题。这种技巧被称为CAS
(Copy And Swap)。
【注意】
- 确保当对象赋值时有良好的行为,可以利用精心编排的语句,或者是
CAS
。 - 确定当函数操作一个以上的对象引用,而且这些对象引用可能指向同一个对象时,其行为仍然正确。