C/C++面试题(3)——剑指offer1(赋值运算符函数)

时间:2023-02-12 10:20:18

今天又复习了C++面试题,这道题的目的是:给出一个类的声明,然后写出这个类的构造函数、析构函数、拷贝构造函数和运算符重载函数。

题目

//题目:如下为类型CMyString的声明,请为该类型添加赋值运算符函数。
class CMyString
{
public:
CMyString(char *pData=NULL);//构造函数
CMyString();//析构函数
private:
char* m_pData;//数据域,字符指针
};

接下来分析如何一步步从最一般的解法到考虑异常安全性的解法:


那么什么是赋值运算符函数呢?

赋值运算符函数,由operator后面跟所定义的操作符符号,通过定义名为operator=函数来对赋值进行定义。该操作符函数有两个形参:第一个形参对应左边的操作数(隐式绑定到this指针了),第二个形参对应右操作数。返回类型应该与内置赋值运算返回的类型相同,内置类型的赋值运算返回对右操作数的引用,赋值操作符也返回对同一类型的引用。例如:
class B
{
public:
B& operator=(const B &);//赋值运算符函数的声明
};

编写赋值运算符函数应该注意的地方:

对于定义一个赋值运算符函数时,需要注意一下几点:

(1)函数的返回类型必须是一个引用,因为只有返回自身的引用(也即 *this),才可以连续赋值;

(2)传入的参数类型声明为常量引用。如果传入的参数不是引用而是实例,那么从形参到实参会调用一次复制构造函数。把参数声明为常量引用,可以提高代码效率,同时赋值运算函数内不会改变传入的实例状态;

(3)一定要记得释放实例自身已有的内存,否则程序容易出现内存泄露

(4)注意传入的参数和当前的实例(*this)是不是同一个实例,如果是同一个,则不用进行赋值操作,直接返回即可。


那么赋值运算符函数的声明是什么呢?

CMyString& operator=(const CMyString& str);//重载运算符

经典的解法如下:
#include<iostream>
#include<stdlib.h>
using namespace std;

class CMyString
{
public:
CMyString(char *pData=NULL);<span style="white-space:pre"></span>//构造函数
CMyString(const CMyString& str);<span style="white-space:pre"></span>//拷贝构造函数
~CMyString();<span style="white-space:pre"></span>//析构函数
CMyString& operator=(const CMyString& str);//重载运算符
void Print();<span style="white-space:pre"></span>//打印字符串
private:
char* m_pData;<span style="white-space:pre"></span>//数据域,字符指针
};

void CMyString::Print()
{
cout<<m_pData<<endl;
}
//构造函数
CMyString::CMyString(char *pData)
{
if(pData==NULL)<span style="white-space:pre"></span>//如果构造函数的参数为空
{
m_pData=new char[1];
m_pData[0]='\0';<span style="white-space:pre"></span>//初始化字符串,内容为'\0'
}
else<span style="white-space:pre"></span>//如果构造函数的参数不为NULL,那么首先求出字符串长度,然后new一个长度为len+1的字符数组
{
int len=strlen(pData);
m_pData=new char[len+1];
strcpy(m_pData,pData);//字符串拷贝
}
}

//析构函数
CMyString::~CMyString()
{
delete[] m_pData;
}

//拷贝构造函数,拷贝构造函数与构造函数的思路非常类似。
CMyString::CMyString(const CMyString& str)
{
int len=strlen(str.m_pData);
m_pData=new char[len+1];
strcpy(m_pData,str.m_pData);
}

//重载运算符
CMyString& CMyString::operator=(const CMyString& str)
{
//如果传入的参数与当前的实例是同一个实例,则直接返回自身
if(this==&str)
return *this;

//释放实例自身已有内存
delete[] m_pData;
m_pData=NULL;

//在删除自身内存以后在重新new一个长度为len+1的字符数组,类似拷贝构造函数
int len=strlen(str.m_pData);
m_pData=new char[len+1];
strcpy(m_pData,str.m_pData);
}

void main()
{
char* strs="hello xuxing";
CMyString str1(strs);
CMyString str2;
str2=str1;
str1.Print();
str2.Print();

system("pause");
}</span>

前面代码的不足之处:
前面函数中,我们在分配内存之前就先调用了delete [] m_pData; 释放了m_pData的内存。但是现在问题出现了,加入我们此时释放了内存,但是剩下的内存又不够我们 new char的空间,这个时候m_pData 将会是一个空指针,这样非常导致程序崩溃。那我们如何解决这个问题呢?由于我们担心先释放会发生内存不够的情况,那么我们就先非配内存,并且判断内存是否非配成功,在分配成功之后,再行释放内存,这样就做到万无一失了。

改进之后的代码:

<span style="font-family:Microsoft YaHei;font-size:14px;">//重载运算符
CMyString& CMyString::operator=(const CMyString& str)
{
if(this!=&str)//
{
CMyString strTemp(str);<span style="white-space:pre"></span>//使用构造函数创建一个临时对象
//交换临时对象与当前对象的m_pData值
char* pTemp=strTemp.m_pData;
strTemp.m_pData=m_pData;
m_pData=pTemp;
}
return *this;
}</span>


改进之后的优点分析:这样的一个好处是在运行完if语句以后,因为除了strTemp的作用于,该实例会自动调用析构函数,把strTemp.m_pData所指向的内存释放掉,而此时strTemp.m_pData指向的是实例原先m_pData指向的内存,并没有释放当前指向的pTemp这一块内存。还有一点是通过构造函数为临时实例分配内存,如果在new char过程中抛出异常,并没有改变该实例m_pData所指向的内容,也没有释放内存,所以是异常安全性的。