我们在类中重载赋值运算符(operatro =)时会出现自我赋值和异常安全的问题,下面就来逐步解决这两个问题。
自我赋值发生在对象被赋值给自己时:
class Person { ... };
Person p;
...
p = p;
这看起来有点蠢,但毫无疑问它是合法的,我们也不要认定这种事情决不会发生。
此外赋值动作并不总是那么可被一眼辨识出来,例如:
arr[i] = arr[j] // 潜在的自我赋值
*pa = *pj // 指针指向引起的自我赋值
int p = 0; // 引用引起的自我赋值
int& rp = p;
...
p = rp;
如果我们写出下面这样的代码:
class Teacher { ... };
class Student
{
...
private:
Teacher* t;
};
Student& Student::operator=(const Student& rt)
{
delete t;
t = new Teacher(*(rt.t));
return *this;
}
这里的自我赋值问题是,opreator=函数的*this(赋值目的端)和 rt有可能是同一个对象。如果这样delete就不只是销毁当前对象的Teacher它也销毁rt的Teacher。在函数最后,Student本不会因为自我赋值而改变,现在却持有一个指针指向一个已被删除的对象。
传统的解决方法就是在operator最前面加一个检测达到自我赋值的检验目的:
Student& Student::operator=(const Student& rt)
{
// 这种检测的方法需要保证Student与Teacher是一对一的关系
// 即不能出现不同Stu的指针指向相同Tea的情况,否则也会引起上述问题
// if (this != &rt)
if (this->t != rt.t)
{
delete t;
t = new Teacher(*(rt.t));
}
return *this;
}
这样做确实具有自我赋值安全性,但并不具备异常安全性。
上述的代码存在异常方面的麻烦,更明确的说,如果new Teacher导致异常(分配时内存不足或因为Teacher的拷贝构造函数抛出异常),Student最终都会持有一个指针指向一块被删除的Teacher。你无法安全地删除它们,甚至无法安全地读取它们。
我们可以采取先申请空间,再删除原有空间的做法。这样,即使申请空间时抛出异常,原有指针也不会受到影响。
Student& Student::operator=(const Student& rt)
{
// 执行检测可以清晰地显示我们考虑了自我赋值这一情况
if (this->t != rt.t)
{
Teacher* tmp = t; // 记住原先的t
t = new Teacher(*(rt.t)); // 令t指向新空间
delete tmp; // 删除原先的t
}
return *this;
}
// 这个版本可以去掉函数最前面的检测,即使没有检测也可以完成自我赋值
// 前提同样是不能出现不同Stu的指针指向相同Tea的情况,否则会引起奇怪的问题
// 这样或许会在自我赋值时效率低,但检测本身就需要成本且发生频率并不高
Student& Student::operator=(const Student& rt)
{
Teacher* tmp = t; // 记住原先的t
t = new Teacher(*(rt.t)); // 令t指向新空间
delete tmp; // 删除原先的t
return *this;
}
我们也可以利用拷贝构造来完成这一函数:
class Student
{
...
void swap(Student& rs); // 交换*this和rs的数据
...
};
Student& Student::operator=(const Student& rs)
{
Student tmp(rs); // 为rs数据制作一份拷贝
swap(tmp); // 将*this数据和tmp的数据交换
// tmp会在出了operator=作用域后自动销毁
return *this;
}
// 若赋值重载采用的是传值传参
Student& Student::operator=(const Student rs)
{
swap(rs); // 将*this数据和rs的数据交换
return *this;
}