自我赋值
有时候我们提供的类客户有可能进行以下误操作:
class Widget { ... };
Widget w;
...
w = w; // 给自己赋值
a[i] = a[j]; // 潜在的自我赋值
*px = *py; // 潜在的自我赋值
即进行自我赋值。
如果两个对象来自同一个继承结构,甚至不需要将它们声明为同一类型,因为基类引用或指针可以引用或指向派生类类型的对象:
class Base { ... };
class Derived : public Base { ... };
void doSomething(const Base& rb, Derived* pd); // rb和*pd可能是同一个对象
如果试图自己管理资源,可能会陷入这样的陷阱:在使用完资源之前不小心释放了资源。
class Bitmap { ... };
class Widget {
public:
Widget& operator=(const Widget& rhs);
...
private:
Bitmap* pb; // 指向一个从堆中分配的对象
};
Widget& Widget::operator=(const Widget& rhs) // 不安全的版本
{
delete pb; // 停止使用当前的 bitmap
pb = new Bitmap(*rhs.pb); // 开始使用rhs的bitmap的一个副本
return *this; // 见条款10
}
*this(赋值的目标)和rhs可以是同一个对象,导致最终指向一个已经被删除的对象。
防止这种错误的传统方法是在operator=的顶部检查是否是对自己赋值:
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs) return *this; // 如果是自我赋值,什么也不做
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
但是它还有异常安全性的问题:如果“new Bitmap”表达式产生异常(因为没有足够的内存用于分配,或者因为Bitmap的拷贝构造函数抛出异常),将最终指向已删除的Bitmap对象。
幸运的是,使operator=具有异常安全性,通常也会使它具有自赋值安全性。
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig = pb; // 记住原先的pb
pb = new Bitmap(*rhs.pb); // 让pb指向*rhs.pb的副本
delete pOrig; // delete原先的pb
return *this;
}
如果“new Bitmap”抛出异常,pb(以及它所在的Widget)保持不变。
拷贝和交换
除了手动排序operator=中的语句,以确保实现异常安全和自我赋值安全,另一种选择是使用称为“拷贝和交换”的技术。
class Widget {
...
void swap(Widget& rhs); // 交换*this和rhs的数据;
... // 详见条款29
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); // 复制rhs的数据
swap(temp); // 将*this的数据与副本的数据交换
return *this;
}
类的复制赋值操作符可以声明为按值接受实参,按值传递某项内容会生成该实参的副本:
Widget& Widget::operator=(Widget rhs) // pass-by-value,rhs是对象的副本
{
swap(rhs); // 将*this的数据与副本的数据交换
return *this;
}
总结
- 当对象被赋值给自身时,确保operator=表现良好。技术途径包括:比较源对象和目标对象的地址、仔细的语句排序、copy-and-swap。
- 确保在两个或多个对象相同(是同一个对象)的情况下,函数的行为仍然正确。