一、我们平时习惯写的赋值操作代码
我们平时可能已经习惯了这样的代码,并且认为是对的:
#include<iostream>
using namespace std;
class Demo{
public:
Demo& operator=(const Demo& x);
Demo(int x=10,int y=15):a(x),b(y){
p=new int;
}
void Print(){
cout<<"a="<<a<<endl;
cout<<"b="<<b<<endl;
}
private:
int a;
int b;
int* p;
};
Demo& Demo::operator=(const Demo& x){
a=x.a;
b=x.b;
delete p;
p=new int(*(x.p));
return *this;
}
int main(){
Demo obj(40,50);
Demo obj2;
obj2=obj;
obj.Print ();
obj2.Print();
return 0;
}
二、存在的问题
确实,以上代码运行能通过,赋值操作也能完美运行。但是,只是表面上看起来合理而已,当出现自我赋值时并不安全(实际上,也不具备异常安全性)。上面代码中,有可能*this和x是同一对象,假如真的如此,delete不仅仅删除了当前对象的Demo,也删除了x的Demo,这样,这函数末尾,持有的指针会指向一个已经删除的对象,(注:但是用obj=obj时,也能运行通过,不知道为什么??,先按书中的来吧)
三、解决方法
为了解决这种错误,我们需要这样做:
Demo& Demo::operator=(const Demo& x){
if(this==&x)//从地址上判断是否指向同一块内存
return *this;
a=x.a;
b=x.b;
delete p;
p=new int(*(x.p));
return *this;
}
上述方法确实能解决自我赋值问题,但是还不具备异常安全性,假设p=new int(*(x.p))出现异常,那么最终会导致Demo持有一个指针指向被删除的一块Int,为了解决异常问题,可以采取下面的策略:
Demo& Demo::operator=(const Demo& x){
if(this==&x)
return *this;//可以省略这段防止自我赋值的代码
a=x.a;
b=x.b;
int* temp=p;//保存旧指针
p=new int(*(x.p));//如果new操作发生异常,则仍然指向原来的空间
delete temp;//删除原先的temp
return *this;
}
注意上述代码中即使没有
if(this==&x)
return *this;
也是行的通的,因为我们对原来的指针做了一个备份,删除旧的,指向新的;
四、最优方法
解决自我赋值和异常问题还有两个版本,即使用所谓的copy and swap技术
Demo& operator= (const Demo& x)
{
Demo temp(x);
swap(*this, temp);
return *this;
}
copy and swap的原理:为你打算修改的对象做出一份副本,然后在那份副本身上做出一切修改,若有任何修改动作抛出异常,原对象仍然保持未修改的状态;待所有改变都成功后,再将修改过的那一个副本和原对象在一个抛出异常给的操作中置换(swap)(具体参见条款29);这里把所有负担都落在了复制构造函数上,如果出现异常,那么swap就不会执行;
另一个版本是
Demo& operator= (const Demo x)
{
swap(*this, temp);
return *this;
}
注意,这个版本和上面的有所不同,参数是通过传值,而传值操作会生成一份复件,即此时的x是原对象的一份副本(会调用赋值构造函数)(传值方式请参考条款20),那么申请临时变量的任务就落在形参上了;这样的方式牺牲了代码的清晰性,而增加了代码的高效;
参考:Effective C++ 3rd(侯捷译)