首先让我们先看一下浅层复制引起的指针悬挂
#include<iostream>
using namespace std;
class STRING
{
public:
STRING(char *s)
{
ptr=new char[strlen(s)+1];
strcpy(ptr,s);
}
void Show()
{
puts(ptr);
}
~STRING()
{
delete[] ptr;
}
private:
char *ptr;
};
int main()
{
STRING str1("aaaa");
STRING str2("bbbb");
str1.Show();
str2.Show();
str1=str2;
// str1.Show();
}
上述程序在运行时会报错,原因就在于str1=str2这句。
程序开始运行时,创建对象str1和str2,分别调用构造函数,通过运算符new分别从内存中动态分配了一块空间,字符指针ptr指向内存空间执行语句str1=str2时,由于用户没有定义赋值运算符函数,所以就调用了默认的赋值运算符函数,使两个对象str1和str2的指针ptr都指向了new开辟的同一空间,这个空间的字符串为“aaaa”。主程序执行结束后,对象逐个撤销,先撤销str2,调用析构函数并用delete释放动态分配的内存空间。然后撤销str1,第二次调用析构函数,尽管这时str1的指针ptr存在,但其所指向的空间却无法访问了,出现了指针悬挂。指针悬挂所引起的问题不止是程序崩溃,还有内存泄露,对象str2中的ptr指针原先所指向的动态空间“bbbb”将一直不会释放,造成内存泄露。
下面是剑指offer中所提出的适于初级程序员的解法
#include<iostream>
using namespace std;
class STRING
{
public:
STRING(char *s)
{
m_pDada=new char[strlen(s)+1];
strcpy(m_pDada,s);
}
~STRING()
{
delete[] m_pDada;
}
void Show()
{
puts(m_pDada);
}
STRING &STRING::operator=(const STRING &s);
private:
char *m_pDada;
};
STRING& STRING::operator=(const STRING &s)
{
if(this==&s)
{
return *this;
}
delete[] m_pDada;
m_pDada=new char[strlen(s.m_pDada)+1];
strcpy(m_pDada,s.m_pDada);
return *this;
}
int main()
{
STRING str1("mybook");
STRING str2("jeep");
STRING str3("aaaa");
str1.Show();
str2.Show();
str3.Show();
str3=str2=str1;
str3.Show();
return 0;
}
书中提出来初级软件工程师要注意的四个点
1.是否把返回值声明为该类型的引用,并在函数结束前返回实例自身的引用。
2.是否把传入参数的类型声明为常量引用。
3.是否释放实例自身已有的内存。
4.是否判断传入的参数和当前实例是不是同一个实例。
还有一点需要注意的就是赋值运算符只能重载为成员函数,而不能重载为友元函数。当然这个错误也很少有人去犯。
下面是一个考虑程序安全性的解法
STRING& STRING::operator=(const STRING &s)
{
if(this!=&s)
{
STRING strTemp(s);
char *pTemp=strTemp.m_pDada;
strTemp.m_pDada=m_pDada;
m_pDada=pTemp;
}
return *this;
}
在前面的函数中,分配内存前线delete释放实例m_Data的内存,如果此时内存不足将导致new char抛出异常,m_Data将是一个空指针。
在下面的函数中,先创建临时变量strTemp,接着把strTemp中的m_Data和实例自身的m_Data交换,由于strTemp是局部变量,当程序运行到if外时,就会自动调用析构函数。
在新的代码中,如果内存不足将导致异常,但是此时我们并没有修改原来实例的状态,这样就保证了安全性。