条款11: 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符
// 一个很简单的String类
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");
如果进行下面的赋值:b=a;
因为没有自定义的operator=可以调用,C++会生成并调用一个缺省的operator=操作符(见条款45)。这个缺省的赋值操作符会执行从a的成员到b的成员的逐个成员的赋值操作,对指针(a.data 和b.data) 来说就是逐位拷贝。
这种情况下至少有两个问题。第一,b 曾指向的内存永远不会被删除,因而会永远丢失。这是产生内存泄漏的典型例子。第二,现在a和b包含的指针指向同一个字符串,那么只要其中一个离开了它的生存空间,其析构函数就会删除掉另一个指针还指向的那块内存。
String a("Hello"); // 定义并构造 a
{ // 开一个新的生存空间
String b("World"); // 定义并构造 b
...
b = a; // 执行 operator=,
// 丢失b的内存
} // 离开生存空间, 调用
// b 的析构函数
String c = a; // c.data 的值不能确定!
// a.data 已被删除
当c.data 用.data的值来初始化时没有内存泄漏,因为c.data没指向任何地方。不过,假如c被a初始化后,c.data和a.data指向同一个地方,那这个地方会被删除两次:一次在c被摧毁时,另一次在a被摧毁时。
拷贝构造函数的情况和赋值操作符有点不同。
void doNothing(String localString) {}
String s = "The Truth Is Out There";
doNothing(s);
localString 离开了其生存空间,调用析构函数。其结果也将是:s包含一个指向localString 早已删除的内存的指针。
用delete去删除一个已经被删除的指针,其结果是不可预测的。
结论:只要类里有指针时,就要写自己版本的拷贝构造函数和赋值操作符函数。
特例:当实现拷贝构造函数和赋值操作符非常麻烦的时候,特别是可以确信程序中不会做拷贝和赋值操作的时候,去实现它们就会相对来说有点得不偿失。可以只声明这些函数(声明为private成员)而不
去定义(实现)它们。
去定义(实现)它们。
条款12: 尽量使用初始化而不要在构造函数里赋值
template<class T>
class NamedPtr {
public:
NamedPtr(const string& initName, T *initPtr);
...
private:
string name;
T *ptr;
};
第一种方法是使用成员初始化列表:
template<class T>
NamedPtr<T>::NamedPtr(const string& initName, T *initPtr )
: name(initName), ptr(initPtr)
{}
第二种方法是在构造函数体内赋值:
template<class T>
NamedPtr<T>::NamedPtr(const string& initName, T *initPtr)
{
name = initName;
ptr = initPtr;
}
推荐第一种,原因:1.有些情况下必须用初始化。特别是const 和引用数据成员只能用初始化,不能被赋值。2.在于效率。
通过成员初始化列表来进行初始化总是合法的,效率也决不低于在构造函数体内赋值,它只会更高效。
特例:大量的固定类型的数据成员要在每个构造函数里以相同的方式初始化的时候。
class ManyDataMbrs {
public:
// 缺省构造函数
ManyDataMbrs();
// 拷贝构造函数
ManyDataMbrs(const ManyDataMbrs& x);
private:
int a, b, c, d, e, f, g, h;
double i, j, k, l, m;
};
//使用初始化列表,难于维护
ManyDataMbrs::ManyDataMbrs()
: a(1), b(1), c(1), d(1), e(1), f(1), g(1), h(1), i(0),
j(0), k(0), l(0), m(0)
{ ... }
//使用赋值初始化
void ManyDataMbrs::init()
{
a = b = c = d = e = f = g = h = 1;
i = j = k = l = m = 0;
}
解释为什么第一种效率更高:
对象的创建分两步:
1. 数据成员初始化。(参见条款13)
2. 执行被调用构造函数体内的动作。
1. 数据成员初始化。(参见条款13)
2. 执行被调用构造函数体内的动作。
(对有基类的对象来说,基类的成员初始化和构造函数体的执行发生在派生类的成员初始化和构造函数体的执行之前)
对NamedPtr类来说,这意味着string对象name的构造函数总是在程序执行到NamedPtr的构造函数体之前就已经被调用了。如果没有为name指定初始化参数,string的缺省构造函数会被调用。当在NamedPtr的构造函数里对name执行赋值时,会对name调用operator=函数。这样总共有两次对string的成员函数的调用:一次是缺省构造函数,另一次是赋值。(初始化了两次,浪费!)