在C++中,有这样一种构造函数:“拷贝构造函数”,这样的函数有什么用呢?
我们先想一下,拷贝构造函数在什么时候会使用:将整个对象实例作为函数参数传递,将整个对象实例作为返回值返回,以及(恕笔者愚昧也不知道还有没有,本文仅仅讨论参数传递所带来的一些问题)。
好,说了半天,还是代码实在一点,我就直接引入一个代码先:
#include <stdio.h>
#include <string.h>
class CMyString{
private:
char* buf;
public:
CMyString(char* str)
{
buf = new char[strlen(str) + 1];
strcpy(buf, str);
}
void Show()
{
printf("%s\n",buf);
}
~CMyString()
{
delete buf;
}
};
int main()
{
CMyString mstr("1234567");
mstr.Show();
return 0;
}
好的,编译运行一下,一切看似没有问题。内存也正常释放了。嗯嗯。风平浪静。但是,这样是危险的。
不知什么时候,你可能写了这样一个函数,将CMyString类型的对象作为函数的参数使用了。这时候问题出现了。以下是加入函数后的代码:
#include <stdio.h>
#include <string.h>
class CMyString{
private:
char* buf;
public:
CMyString(char* str)
{
buf = new char[strlen(str) + 1];
strcpy(buf, str);
}
void Show()
{
printf("%s\n",buf);
}
~CMyString()
{
delete buf;
}
};
void ShowMyString(CMyString str)
{
str.Show();
}
int main()
{
CMyString mstr("1234567");
ShowMyString(mstr);
mstr.Show();
return 0;
}
好嘞,运行。突入起来的弹窗打破了平静。挂了。
这是怎么回事?好好地代码,怎么会突然出现这样的问题。有经验的程序*们可能以下就能够看出问题所在,不过有经验的同行们有可能就不会细看小弟的博文了(当然也欢迎各路高手前来指教)。
选择“retry”,程序停在了某个地方:
从代码那些注释上看来(不是在/**/里面的才叫注释,代码本身也是注释,如果这个代码写的好的话。),是从堆中释放内存出了什么岔子。好吧,我承认,C++你又调皮了。我们还是看看问题在哪儿吧。
略去中间一堆调试的过程,原谅笔者太懒,不愿意把自己发现问题个过程分享出来。不过,容我慢慢解释。
问题就在我们调用函数的地方以及我们的析构函数。你可能已经发现了,在将对象作为函数参数传递时(传值),该对象的拷贝构造函数会被调用,以构造一个对象的副本,将新构造的对象作为参数传入函数。当然,在函数结束的时候,该副本也就该入棺材了,因此,析构函数被调用,对象被析构。
问题就来了,在ShowMyString里面,析构函数会被调用以销毁str,也就是使用了delete释放str指向的堆空间。函数返回,这时候一切正常。但是,当我们的main函数结束的时候,再次调用析构函数销毁mstr,也会使用delete,不对?这个对象里面的buf已经被释放过了啊?嗯嗯。因此便出现了刚才的问题。
这也说明了一点,默认的拷贝构造函数在某些时候是不靠谱的,它的拷贝仅仅是一种浅拷贝,仅仅是将对象占用的那段空间拷贝了一下。因此,两次析构delete的buf是同一段堆空间,也就是那段空间被释放了两次,问题就出现了。
那么,如何避免呢?扯了半天,看起来我们已经跑题了,还是回归正题来吧。我们可以向函数传入指针,或者传入引用,以避免这个过程中的拷贝与析构,这样问题也就解决了。当然,你可能比较倔,或者有强迫症,非要整个对象传值进去,那也不是没办法。
加入拷贝构造函数吧。代码如下:
#include <stdio.h>
#include <string.h>
class CMyString{
private:
char* buf;
public:
CMyString(char* str)
{
buf = new char[strlen(str) + 1];
strcpy(buf, str);
}
void Show()
{
printf("%s\n",buf);
}
CMyString(CMyString& str)
{
buf = new char[strlen(str.buf) + 1];
strcpy(buf, str.buf);
}
~CMyString()
{
delete buf;
}
};
void ShowMyString(CMyString str)
{
str.Show();
}
int main()
{
CMyString mstr("1234567");
ShowMyString(mstr);
mstr.Show();
return 0;
}
运行,问题引刃而解了。
不会再出现那个犯人的错误了。
我们可以总结以下问题:如果对象中存在堆中分配的空间的指针(不仅仅是堆空间,比如打开了一个文件,一个句柄,等等),那么析构的时候delete会引发一些问题,比如上面的问题。传递参数是原因之一,当然也有其它问题,C++博大精深,难以一下说明白。写一个拷贝构造函数就显得比较重要了,有时候还需要重载=运算之类的,笔者也不能很好的说清楚。
这也再次说明了一个问题,拷贝构造函数还是有必要的,以及写C++代码时要多考虑。
谢谢。