条款 11: 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符
前言
几乎所有的类都有一个或多个构造函数,一个析构函数和一个赋值操作符。
构造函数控制对象生成时的基本操作,并保证对象被初始化;析构函数摧毁一个对象并保证它被彻底清除;赋值操作符则给对象一个新的值。在这些函数上出错就会给整个类带来无尽的负面影响,所以一定要保证其正确性。
class String {
public:
String(const char *value);
~String();
... // 没有拷贝构造函数和 operator=
private:
char *data;
};
String::String(const char *value)
{
if (value) {
data = new char[strlen(value) + 1];
strcpy(data, value);//从常量区拷贝字符串到堆区
} else {
data = new char[1];
*data = '\0';
}
}
inline String::~String() { delete [] data; }
如果这样定义两个对象:
String a("Hello");
String b("World");
其结果就会如下所示:
a: data——> "Hello\0" //对象 a 的内部是一个指向包含字符串"Hello"的内存的指针
b: data——> "World\0"//对象b类似对象a
赋值操作符
如果进行下面的赋值:b = a; 会怎样?
因为没有自定义的operator=,C++会生成并调用一个缺省的 operator=操作符(见条款 45)。
缺省的赋值操作符会执行从 a 的成员到 b 的成员的逐个成员的赋值操作,
对指针(a.data 和 b.data) 来说就是逐位拷贝,指针本身值拷贝,并不是将指针指向的内容拷贝。
问题1. b 曾指向的内存永远不会被删除,因而会永远丢失。这是产生内存泄漏的典型例子。
问题2.a 和 b 包含的指针指向同一内存空间,只要其中一个离开了它的生存空间,
其析构函数就会删除掉另一个指针还指向的那块内存。
拷贝构造函数
String a("Hello"); // 定义并构造 a
{
String b("World"); // 定义并构造 b
...
b = a; // 执行 operator=,丢失 b 的内存
} // b离开生存空间, 调用析构函数,此时会影响到对象a,因为b析构时会删除a中data指针指向的内存
String c = a; //拷贝构造函数, c.data 的值不能确定!
// a.data 已被删除
因为没有自定义的拷贝构造函数,c++也会生成并调用缺省的拷贝构造函数对对象进行逐位拷贝。这会导致同样的问题,但不用担心内存泄漏,因为被初始化的对象还不能指向任何的内存。不过,假如 c 被 a 初始化后,c.data 和 a.data 指向同一个地方,那这个地方会被删除两次:一次在 c 被摧毁时,另一次在 a 被摧毁时。
拷贝构造函数的情况和赋值操作符还有点不同。在传值调用的时候,它会产生问题。当然正如条款 22 所说明的,一般很少对对象进行传值调用,但还是看看下面的例子:
void doNothing(String localString) {}
String s = "The Truth Is Out There";
doNothing(s);
一切好象都很正常。但因为被传递的 localString 是一个值,它必须从 s 通过(缺省)拷贝构造函数进行初始化。于是 localString 拥有了一个 s 内的指针的拷贝。当 doNothing 结束运行时,localString 离开了其生存空间,调用析构函数。其结果也将是:s 包含一个指向 localString 早已删除的内存的指针。
顺便指出,用 delete 去删除一个已经被删除的指针,其结果是不可预测的。
所以即使 s 永远也没被使用,当它离开其生存空间时也会带来问题。
解决这类指针混乱问题的方案在于,只要类里有指针时,就要写自己版本的拷贝构造函数和赋值操作符函数。
在这些函数里,你可以拷贝那些被指向的数据结构,从而使每个对象都有自己的拷贝;或者可以采用某种引用计数机制(见条款 M29)去跟踪当前有多少个对象指向某个数据结构。引用计数方法更复杂,而且它要求构造函数和析构函数内部做更多的工作,但在某些(虽然不是所有)程序里,它会大量节省内存并切实提高速度。
对于有些类,当实现拷贝构造函数和赋值操作符非常麻烦的时候,特别是可以确信程序中不会做拷贝和赋值操作的时候,去实现它们就会相对来说有点得不偿失。那当现实中去实现它们又不切实际的情况下,该怎么办呢?
很简单,照本条款的建议去做:可以只声明这些函数(声明为 private 成员)而不去定义(实现)它们。这就防止了会有人去调用它们,也防止了编译器去生成它们。关于这个俏皮的小技巧,参见条款 27。