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; }
请注意这个类里没有声明赋值操作符和拷贝构造函数。这会带来一些不良后果:内存泄露和重复delete
String a("Hello");
String b("World");
b = a;
存在两个问题:
第一,b 曾指向的内存永远不会被删除,因而会永远丢失。这是产生内存泄漏的典型例子。第二,现在 a 和 b 包含的指针指向同一个字符串,那么只要其中一个离开了它的生存空间,其析构函数就会删除掉另一个指针还指向的那块内存。
当然正如条款 22 所说明的,一般很少对对象进行传值调用。拷贝构造函数的情况和赋值操作符还有点不同。在传值调用的时候,它会产生问题。
void doNothing(String localString) {}
String s = "The Truth Is Out There";
doNothing(s);
一切好象都很正常。但因为被传递的 localString 是一个值,它必须从 s 通过(缺省)拷贝构造函数进行初始化。于是
localString 拥有了一个 s 内的指针的拷贝。当 doNothing 结束运行时,localString 离开了其生存空间,调用析构函数。其结果也将是:s 包含一个指向 localString 早已删除的内存的指针。
解决这类指针混乱问题的方案在于,只要类里有指针时,就要写自己版本的拷贝构造函数和赋值操作符函数。在这些函数里,你可以拷贝那些被指向的数据结构,从而使每个对象都有自己的拷贝;或者你可以采用某种引用计数机制(见条款M29)去跟踪当前有多少个对象指向某个数据结构。引用计数的方法更复杂,而且它要求构造函数和析构函数内部做更多的工作,但在某些(虽然不是所有)程序里,它会大量节省内存并切实提高速度。对于有些类,当实现拷贝构造函数和赋值操作符非常麻烦的时候,特别是可以确信程序中不会做拷贝和赋值操作的时候,去实现它们就会相对来说有点得不偿失。前面提到的那个遗漏了拷贝构造函数和赋值操作符的例子固然是一个糟糕的设计,那当现实中去实现它们又不切实际的情况下,该怎么办呢?很简单,照本条款的建议去做:可以只声明这些函数(声明为
private 成员)而不去定义(实现)它们。这就防止了会有人去调用它们,也防止了编译器去生成它们。关于这个俏皮的小技巧的细节,参见条款 27。
在两个调用 new 的地方都小心地用了[],尽管有一个地方实际只需要单个对象。正如条款 5 所说,在配套使用 new 和 delete 时一定要采用相同的形式,所以这里也这么做了。一定要经常注意,当且仅当相应的 new 用了[]的时候,delete才要用[]。