考虑异常安全的赋值运算符

一、赋值运算符需要考虑的因素

一个赋值运算符需要考虑的因素,有以下四点:

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)如此便完成了赋值工作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章