一、赋值运算符需要考虑的因素
一个赋值运算符需要考虑的因素,有以下四点:
1. 检查返回值类型是否是该类类型的引用,并且返回值也是自身引用(即*this)。
- 如果返回类型是一个void,那么就不能进行连续赋值(s1=s2=s3)。s1=s2=s3实际上是s1=(s2=s3),首先是s3对s2进行赋值,赋值完毕之后,会返回一个NULL,这就相当于s1=NULL,这样是C++不允许的,所以会出现编译错误。
- 如果返回值不是一个引用,那么会多调用一次拷贝构造和析构函数,程序的效率相对而言会更低
2. 检查函数的形参是否是const引用类型。
因为如果传入的参数不是引用,而是一个实例的话,那么在实参对形参结合的这个过程中会调用拷贝构造函数。而如果是一个引用就会避免这种无端的销毁,从而提高效率。之所以声明为const,是因为在赋值运算符中,我们可以保证不会对对象进行修改。
3. 检查是否释放掉自身已有内存。
如果我们忘记在赋值之前释放掉自身已有内存,那么就会出现内存泄漏。
4. 检查是否判断自我赋值
如果没有判断是否为自我赋值,那么当是同一个对象进行赋值时,释放掉自身对象,其实也是将传入参数的内存也释放掉了,因此就找不到要赋值的内容了。
二、赋值运算符的常用写法
根据上面的四点,我以模拟String类的赋值运算符为例,我们可以写出下面的赋值运算符重载函数:
String& operator=(const String& s)
{
if (this != &s) //判断是否自我赋值
{
delete[] _str; //释放自身原有空间
char* pStr = new char[s._capacity + 1];
strcpy(pStr, s._str);
_str = pStr;
}
return *this;
}
上面的赋值运算符重载函数有问题吗?
乍一看是没有问题的,但是假如我们深入的考虑的话,就会发现一个很严重的问题。如果我们使用new时空间不足,那么new就会抛出异常。那么pStr就是一个空指针,这样很容易导致程序崩溃,这就是一个异常安全问题。
解决办法:
方法一:先用new分配新空间,再使用delete释放原空间。如下所示:
String& operator=(const String& s)
{
if (this != &s) //判断是否自我赋值
{
char* pStr = new char[s._capacity + 1];
strcpy(pStr, s._str);
delete[] _str; //释放自身原有空间
_str = pStr;
}
return *this;
}
这样在new分配成功后才释放空间,也就是在内存分配失败时,我们可以保证对象不会被修改。
方法二:先创建一个临时对象,然后再将这个临时对象与原来的对象进行交换。
String& operator=(const String& s)
{
if (this != &s) //判断是否自我赋值
{
String s1(s);
char* pStr=s1._str;
s1._str=_str;
_str=pStr;
}
return *this;
}
在上面的代码中,首先创建一个临时对象s1,并且调用拷贝构造函数用传入的形参s为s1初始化。然后将s1._str与自身的_str进行交换。当运行出s1的作用域时,s1会被销毁,因为这时s1._str指向的内存就是自身原来的内存,所以销毁的相当于自身原来的空间。过程如下图所示:
(1)开始时,创建临时对象
(2)执行交换语句后。
(3)s1出作用域,临时对象被销毁
(4)如此便完成了赋值工作