这是一道C++的面试题,下面在这篇博客中分析一下这个问题。先上题目:
//题目:如下为类型CMyString的声明,请为该类型添加赋值运算符函数。 class CMyString { public: CMyString(char *pData=NULL);//构造函数 CMyString(const CMyString& str);//拷贝构造函数 ~CMyString();//析构函数 private: char* m_pData;//数据域,字符指针 };
拿到这个题目,如果你看过effective C++这本书,代码应该很容易就能够完成,在写的时候要注意下面的一个要点:
1>赋值运算符要返回该实例自身的引用(*this),因为只有返回一个引用才可以允许连续赋值操作。
2>把传入的参数的类型声明为常量引用,这样能够避免传入实例时的值拷贝,避免复制构造函数的调用。同时赋值运算符不会改变传入的实例的状态,因此要在传入的参数前加上const关键字。
3>要在分配新的内存之前释放自身已有的空间,否则程序会出现内存泄露。赋值运算符执行的是赋值操作,在赋值运算符之前肯定调用过对象的构造函数或者拷贝构造函数,因为一个对象在实例化的时候肯定会有一个初始化的过程。这里必须释放原来的堆内存空间,因为m_pData可能变得更长了,原来的空间可能已经不够用了。
4>判断传入的参数和当前的实例(*this)是不是同一个实例,如果是同一个实例的话就直接返回,防止str1=str1这样的没有意义的操作。如果不判断这个 ,结果很严重,如果释放了实例的堆内存空间,也就等于释放了参数的堆空间(虽然参数是const的,但是这次的释放是通过this对象释放的,不是通过参数对象释放的),就没有了这个对象的完整备份,那么这个对象就永远的从这个世界上消失了。
CMyString& CMyString::operator=(const CMyString& str) { if(this == &str) { return *this; } delete[] m_pData; m_pData = NULL; m_pData = (char*)malloc(strlen(str.m_pData) + 1); strcpy(m_pData, str.m_pData); return *this; }
上面的代码虽然能够解决问题,但是存在安全隐患,在分配内存之前先用delete释放了实例m_pData的内存,如果此时内存不足,导致new char申请内存失败,m_pData将是一个空指针,这很容易发生异常,因为我们认为它不是一个空指针。
这里要处理的是防止new堆内存的时候,由于内存不足等原因导致异常,而是当前对象的完整性遭到破坏。也就是说你要异常要在操作当前对象之前,不能在操作当前对象的过程中异常,使得这个当前对象不完整。
解决的办法有两种:
1>先用new分配新内容,然后再用delete释放已有内容,也就是分配成功之后再分配新的内容,这样就能保证在分配失败之后,原来的CMyString的实例不会被修改。
2>先在栈中创建一个临时实例,然后在交换临时实例和原有实例
下面给出2>的代码实现:
CMyString& CMyString::operator=(const CMyString& str) { if(this == &str) { return *this; } CMyString tempStr(str); //交换临时实例和原有实例的指针指向 char *pData = tempStr.m_pData; tempStr.m_pData = m_pData; m_pData = pData; return *this; }
注意代码的技巧的地方在于,这个tempStr是在栈中开辟的临时的实例,在这个函数运行结束以后,这个函数栈要销毁,所以tempStr的析构函数会自动的调用,当析构函数调用的时候肯定会自动的释放原来的那块堆内存的。
在新的代码中,在CMyString的构造函数里用new分配内存。如果由于内存不足抛出如bad_alloc等异常,我们还没有修改原来的实例的状态,因此实例的状态还是有效的。
其实上面的代码应该有try-catch模块,这样就能处理异常,同时不会是程序终止。改进的实现的唯一目的就是为了保证在抛出异常(可能因为内存不足)时,原来的实例没有被修改过。也就是说,要抛异常你就早点抛,在我修改原来实例之前就抛,要么你就不要抛。