异常安全的CMyString赋值运算符函数重载
扫描二维码
随时随地手机看文章
问题:给出如下CMyString的声明,要求为该类型添加赋值运算符函数。
class CMyString
{
public:
CMyString(char* pData = NULL);
CMyString(const CMyString& str);
~CMyString(void);
private:
char* m_pData;
};
当面试官要求应聘者定义一个赋值运算符函数时,他会关注如下几点:
· 是否把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身(即*this)的引用?只有返回一个引用,才可以允许连续赋值。否则如果函数的返回值是void,假设有三个CMyString的对象,str1、str2和str3,在程序中语句str1=str2=str3将不能通过编译。
· 是否把传入的参数的类型声明为常量引用?如果传入的参数不是引用而是实例,那么从形参到实参会调用一次构造拷贝函数。把参数申明为引用可以避免这样的无谓消耗,能提高代码的效率。同时,我们在赋值运算符函数内是不会改变传入的实例的状态的,因此应该为传入的引用参数加上const关键字。
· 是否记得释放实例自身已有的内存?如果忘了在分配新内存之前释放自身已有的空间,将出现内存泄露。
· 是否判断传入的参数是不是和当前的实例(*this)是不是同一个实例?如果是同一个,则不进行赋值操作,直接返回。如果事先不判断,就进行赋值,那么在释放实例自身的内存的时候就会导致严重的问题:当*this和传入的参数是同一个实例时,那么一旦释放了自身的内存,传入的参数的内存也同时被释放了,因此再也找不到需要赋值的内容了。
当我们完整地考虑了上述几方面之后,我们可以写出如下的代码:
CMyString& CMyString::operator =(const CMyString &str)
{
if(this == &str)
return *this;
delete []m_pData;
m_pData = NULL;
m_pData =
newchar[strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
return *this;
}
这是一般C++教材上提供的参考代码。如果是面试的是应届毕业生或者C++初级程序员,如果能全面地考虑到前面四点并完整地写出代码,面试官可能会让他通过这轮面试。但如果面试的是C++的高级程序员,面试官可能会提出更高的要求。
面试官会提醒我们在前面的函数中,显示地用delete释放自身m_pData的内存。同时我们也会在析构函数中用delete释放自身m_pData的内存。如果这个类型中添加新的指针成员变量,那么我们至少需要做两处修改,即同时在析构函数和这个赋值运算符函数里添加一条delete语句来释放新指针所指向的内存。一个改动需要在代码中多个地方修改代码,通常是有安全隐患的。通常我们会记得在析构函数里用delete释放指针成员变量,但未必每次都记得到赋值运算符函数来添加代码释放内存。
我们知道,在分配内存时有可能发生异常。当执行语句new char[strlen(str.m_pData) + 1]发生异常时,程序将从该赋值运算符的重载函数退出不再执行。注意到这个时候语句delete[]m_pData 已经执行了。也就是说赋值操作没有完成,但原来对象的状态已经改变。也就是说不满足题目的异常安全的要求。(释放原来的了,但是没有新建新的)为了满足异常安全这个要求,一个简单的办法是掉换new、delete
的顺序。先把内存new出来用一个临时指针保存起来,只有这个语句正常执行完成之后再执行delete。这样就能够保证异常安全了。
更好的办法在复制运算符函数中利用析构函数自动释放实例已有的内存。下面是这种思路的参考代码:
CMyString& CMyString::operator =(const CMyString &str)
{
if(this != &str)
{
CMyString strTemp(str);
char* pTemp = strTemp.m_pData;
strTemp.m_pData = m_pData;
m_pData = pTemp;
}
return *this;
}
在这个函数中,我们定义一个临时实例strTemp,并把strTemp的m_pData指向当前实例(*this)的m_pData。由于strTemp是个局部变量,但程序员运行到if的外面是也就出了的该变量的域,就会自动调用strTemp的析构函数,就会把strTemp.m_pData所指向的内存释放掉。由于strTemp.m_pData指向的内存就是当前实例之前m_pData的内存。这就相当于自动调用析构函数释放当前实例的内存。如果新增加指针成员变量,我们只需要在析构函数里正确地释放,而不需要对赋值运算符函数做任何修改。
该方案通过调用构造拷贝函数创建一个临时对象来分配内存。此时即使发生异常,对原来对象的状态没有影响。交换临时对象和需要赋值的对象的字符串指针之后,由于临时对象的生命周期结束,自动调用其析构函数释放需赋值对象的原来的字符串空间。整个函数不需要显式用到new、delete,内存的分配和释放都自动完成,因此代码显得比较优雅。