文章目录
1. 题目来源
《剑指-Offer》第二版,P25,面试题1:赋值运算符函数
2. 题目说明
如下为类型CMyString
的声明,请为该类型添加赋值运算符函数:
class CMyString {
public:
CMyString(char* pData = nullptr);
CMyString(const CMyString& str);
~CMyString(void);
private:
char* m_pData;
};
3. 题目解析
3.1 C++六大默认成员函数
首先复习 C++
的六大默认成员函数:构造、拷贝构造、析构、赋值操作符重载、取地址操作符重载、const
取地址操作符重载
3.2 C++运算符重载及5点注意
再复习下运算符重载:
C++
为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,
函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字 operator
后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如
operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型
+
,不 能改变其含义 - 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
- 操作符有一个默认的形参
this
,限定为第一个形参 .* 、:: 、sizeof 、?: 、.
注意以上5个运算符不能重载。这个经常在笔试选择题中出现
3.3 C++赋值运算符及5点注意
现在开始看看赋值运算符,赋值运算符主要有 5 点需要注意:
- 参数类型
- 主要考虑是否把传入的参数的类型声明为常量引用。如果传入的参数不是引用而是实例,那么从形参到实参会调用一次拷贝构造函数。把参数声明为引用可以避免这样的无谓消耗,能提高代码的效率。同时,我们在赋值运算符函数内不会改变传入的实例的状态,因此应该为传入的引用参数加,上const关键字。
- 返回值类型
- 是否把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身的引用(即
*this
)。只有返回一个引用,才可以允许连续赋例自身的引用(即*this)。只有返回一个引用,才可以允许连续赋赋值。假设有 3 个CMyString
的对象:str1、 str2 和str3
,在程序中语句str1=str2=str3
将不能通过编译。
- 检测是否自己给自己赋值
- 是否判断传入的参数和当前的实例(
*this
)是不是同一个实例。如果是同一个,则不进行赋值操作,直接返回。如果事先不判断就进行赋值,那么在释放实例自身的内存的时候就会导致严重的问题:当*this
和传入的参数是同-一个实例时,那么一旦释放了自身的内存,传入的参数的内存也同时被释放了,因此再也找不到需要赋值的内容了。
- 释放自己的内存
- 是否释放实例自身已有的内存。如果忘记在分配新内存之前释放放自身已有的空间,程序将出现内存泄露。
- 避免浅拷贝问题
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝,即默认的使用是浅拷贝,也就是说将该对象的内存原封不动地挪动到新对象的内存中,因此对于含有指针的类,我们往往需要自己实现
copying
的操作来完成深拷贝(除非你就需要以浅拷贝的方式完成该操作),否则很有可能造成有多个指针指向同一块空间,在析构时候同一块空间析构多次导致崩溃。
3.4 初级程序员写法
经过上面的复习和深思熟虑,可以写出下面的经典写法,在《剑指-Offer》描述中就是:适用于初级程序员…
CMyString& CMyString::operator=(const CmyString &str) {
if (this == &str)
return *this;
delete []m_pData;
m_pData = nullptr;
m_pData = new char[strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
return *this;
};
对于初级程序员 OK~
,但还达不到高级程序员的标准
3.5 考虑异常安全性的解法,高级程序员必备
上述代码中,是先用 delete
释放之前实例m_pData
的内存再开辟新空间,==如果此时内存不足导致 new
时抛出异常,那么此时 m_pData
已经为空指针==,容易导致程序崩溃,这样违背了异常安全性(Exception Safety)的原则。因此可以采用先 new
分配新空间,分配成功后再 delete
释放原来的内容,当然书上给出了一个更好的方法,先创建一个临时实例,再交换临时实例和原来的实例,参考代码如下:
CMyString& CMyString::operator=(const CMyString &str) {
if(this !=&str) {
CMyString strTemp(str); // 先创建一个临时对象strTemp
char* pTemp=strTemp.m_pData;
strTemp.m_pData=m_pData; // 再把strTemp.m_pData和自身的m_pData进行交换。
m_pData=pTemp;
// 由于stremp是个局部对象,运行到if作用域外,就会自动调用strtemp的析构函数从而完成了内存的释放。
// 由于strTemp.m_pData指向的内存就是m_pData的内存,就相当于自动调用析构函数释放实例的内存。
// 同时也完成了相应的拷贝工作。
}
return *this;
}
在新的代码中,在CMyString
的构造函数里用new
分配内存。如果由于内存不足抛出诸如bad_ alloc
等异常,我们还没有修改原来实例的状态,因此实例的状态还是有效的,这也就保证了异常安全性。
理解到此地,面试就通过了。