Effective C++条款11: 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符
示例:
class MyString
{
public:
MyString(const char* value)
{
if(value != NULL)
{
data = new char[strlen(value)+1];
strcpy(data, value);
}
else
{
data = new char[1];
*data = '\0';
}
}
~String()
{
delete[] data;
}
private:
char* data;
};
此时MyString类里没有声明赋值操作符和拷贝构造函数,这会带来一些不良后果。
例如:
MyString a("Hello");
MyString b("World");
b = a;
由于没有自定义的operator=可以调用,C++会生成并调用一个缺省的operator=操作符。这个缺省的赋值操作符会执行从a的成员到b的成员的逐个成员的赋值操作,对指针(a.data和b.data)来说就是逐位拷贝。
这种情况下至少有两个问题。第一,b曾指向的内存永远得不到释放。这是产生内存泄露的典型例子。第二,现在a.data和b.data指向同一个字符串,那么只要其中一个离开了它的生命周期,其析构函数就会删除掉另一个指针还指向的那块内存。(/by me 即产生野指针)
拷贝构造函数的情况和赋值操作符类似,但也有不同。在传值调用的时候,它会产生问题。
看下面例子:
void doNothing(MyString localString){}
MyString s = "The Truth Is Out There";
doNothing(s);
一切好像都很正常。但因为被传递的localString是一个值,它必须从s通过(缺省)拷贝构造函数进行初始化。于是localString拥有了一个s内的指针(data)的拷贝。当doNothing结束运行时,localString离开了其生存空间,调用析构函数。其结果也将是:s包含了一个指向localString早已删除的内存的指针。(野指针)
解决方案:
解决这类指针混乱问题的方案在于:(1)只要类里有指针时,就要写自己版本的拷贝构造函数和赋值操作符函数。在这些函数里,程序员可以拷贝那些被指向的数据结构,从而使每个对象都有自己的拷贝;(2)或者可以采用某种引用计数机制去跟踪当前有多少个对象指向某个数据结构。(2)方法更复杂,而且要求构造函数和析构函数内部做更多的工作,但是在某些(虽然不是所有)程序里,它会大量节省内存并切实提高速度。
对于有些类,当实现拷贝构造函数和赋值操作符非常麻烦的时候,特别是可以确信程序中不会做拷贝和赋值操作的时候,去实现它们就会相对有点得不偿失。前面提到的那个遗漏了拷贝构造函数和赋值操作符的例子固然是一个糟糕的设计,那当现实中去实现它们又不切实际的情况下,改怎么办?很简单,照本条款的建议去做:可以只声明这些函数(声明为private成员)而不去定义(实现)它们。这就防止了会有人去调用它们,也防止了编译器去生成它们。关于这个俏皮的小技巧的细节,参见条款27。