您的位置:首页 > 编程语言 > C语言/C++

重载赋值符——谈C++中一个稍微有点复杂的问题

2009-07-06 21:50 309 查看
(以下内容节选自我未曾发表的一本关于C++的书中的部分内容,希望有兴趣的朋友共同研究学习)

上一节向读者介绍了运算符重载,本书前面还介绍过拷贝构造函数的一些内容。在本小节中,笔者将结合这两部分知识,向读者介绍一些有趣且实用的内容。
读者应当记得,在介绍拷贝构造函数时,笔者强调当类的数据成员中包含有指针类型时,最好不要使用默认拷贝构造函数。原因在于默认拷贝构造函数实现的只能是浅拷贝,而浅拷贝会带来数据安全方面的隐患。在C++语言程序设计中,如果某个类的对象持有动态分配的内存资源,那么当这个类的对象发生复制过程的时候,这个过程就可以叫做深拷贝,反之如果对象存在资源,当用户没有为该类定义拷贝构造函数时,编译器就会自动生成默认的拷贝构造函数,这时复制过程并未复制资源,这种情况就是浅拷贝。
除了利用拷贝构造函数来进行对象复制以外,还可以使用重载的赋值运算符来进行对象复制。这种用来进行对象复制的重载赋值运算符称为拷贝赋值运算符。使用拷贝赋值运算符时会使程序的语义更清晰,但是当类对象持有动态分配的内存资源时,拷贝赋值运算符也会遭遇拷贝构造函数同样的问题。也就是说,如果程序员没有为该类定义拷贝赋值运算符,则当对象进行拷贝赋值时,就可能发生浅拷贝。这样会给程序带来安全隐患,可能出现内存泄漏、重复释放内存等诸多问题。
如果程序员定义了拷贝构造函数和拷贝赋值运算符中任何一个,编译器就不会再使用默认的浅拷贝复制语义。而且,通常情况下的初始化复制和对象已存在情况下的复制总是遵循相同的方式,因此如果程序员重新定义了其中任何一个函数,而未定义另外一个,那么复制语义的一致性就没有办法保证。特别地,如果程序员对拷贝构造函数和拷贝赋值运算符定义不恰当,就有可能出现对象复制语义错误等问题。
上一章已经对拷贝构造函数做了介绍,下面我们将对拷贝赋值运算符及其可能出现的安全问题进行剖析。在拷贝赋值运算符安全漏洞的主要表现形式中,拷贝赋值运算符定义不恰当是引起对象复制语义错误等问题的最主要原因。而且拷贝赋值运算符较拷贝构造函数所要面对的安全问题还更复杂些。众所周知,当使用拷贝构造函数拷贝构造对象时,因为新对象还未存在,所以不用释放和重新分配内存资源。而当使用拷贝赋值运算符对对象执行拷贝赋值操作时,因为新对象已经构造完毕,所以该函数的定义中必须对被赋值对象执行释放和重新分配内存的操作。如若不然,就会导致内存泄漏和重复释放内存等错误。
请读者来看下面这段示例代码,其中声明了一个String类。
#include<iostream>
#include<string>

using namespace std;

class String
{
char * data;

public:

//构造函数
String (const char * value)
{
if(value)
{
data=new char [strlen(value)+1];
strcpy (data, value);
}
else
{
data=new char [1];
*data='/0';
}
}

//拷贝构造函数
String(const String& rhs)
{
data=new char [strlen(rhs.data)+1];
strcpy(data, rhs.data);
}
//拷贝赋值运算符
String & operator = (const String& rhs);

//析构函数
~String()
{
delete [] data;
}

void display() const
{
cout<< data << endl;
}
};

//此处暂时略去拷贝赋值运算符的具体实现

int main()
{
String s1 ("Good morning!");
String s2 ("Good luck!");
s2= s1;
s2.display();

return 0;
}
上述代码中略去了拷贝赋值运算符的实现部分。读者不妨自己尝试着写写。下面,给出一些可能的实现方式,并分析其中存在的漏洞。
第一种可能的实现方法如下:
String & String::operator= (const String & that)
{
if(this == &that)
return * this;
data = new char [strlen(that.data)+1];
strcpy(data, that.data);
return *this;
}
读者是否看出了该种实现方法中存在的问题呢,其中的漏洞要比后两种更隐蔽。我们注意到由于未对被赋值对象执行释放内存操作,就为其重新分配了内存,于是即造成被赋值对象原先持有的堆内存被丢弃,并且再也无法被释放,从而导致内存泄漏。
第二种可能的实现方法如下:
String & String::operator= (const String & that)
{
if(this == &that)
return * this;
delete [] data;
strcpy(data, that.data);
return * this;
}
这种方法中存在的漏洞更显著一些。在Visual C++ 6.0环境下编译该程序时,一切正常,运行程序,控制台上也会输出正确的结果,但是随即系统就会报错,显然程序存在致命的错误。分析之后,可知,由于对被赋值对象执行了内存释放操作,但此后却没有为其按照赋值对象的数据大小重新分配内存,结果造成被赋值对象的内部数据指针不再指向合法的堆空间。当执行字符串复制操作时,程序出现了非法访问内存空间的错误,这就是问题所在。
第三种可能的实现方法如下:
String & String::operator = (const String & that)
{
if(this == &that)
return * this;
strcpy(data, that.data);
return *this;
}
同上,在Visual C++ 6.0环境下编译该程序时,一切正常,运行程序,控制台上也会输出正确的结果,但是随即系统就会报错,可见程序存在错误。分析可知,由于没有对被赋值对象执行释放和重新分配内存的操作,因此当主函数执行到语句赋值语句时,s1内部数据指针指向的字符串的长度超出了对象s2被分配的堆内存空间所能容纳的范围,造成数据访问越界错误。
啊!犯了这么多次错误,终于该给出正确的实现方法了。一般情况下,在C++中某个类的对象持有非自动释放的内存资源,程序员应当为该类按照如下顺序实现拷贝赋值运算符:首先,保存原先的资源;然后,分配新的资源;再然后,执行句柄赋值操作;最后,释放原先的资源。下面给出正确的拷贝赋值运算符实现代码。
String & String::operator= ( const String & that)
{
if(this == &that)
return * this;
char*olddata=data; //保存原先的资源
char* newdata=new char [strlen(that.data)+1]; //分配新的资源
strcpy(newdata, that.data);
data = newdata; //执行句柄赋值操作
delete [] olddata; //释放原先的资源
return * this;
}
读者应当注意到,在应用拷贝赋值运算符时必须小心谨慎,稍有大意就会犯各种各样奇怪的错误!希望读者通过本小节的学习能够深切地领悟到这一点。

如果想和作者共同学习C/C++,欢迎点击链接http://student.csdn.net/invite.php?u=113322&c=a139a65a1494291d和笔者成为好友!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: