class Name {
const char* s;
// ...
};
class Table {
Name* p;
size_t sz;
public:
Table(size_t s = 15) { p = new Name[sz = s]; }
~Table() { delete[] p; }
Name* lookup(const char*);
bool insert(Name*);
};
如果t1和t2都是类Table的对象,t1 = t2的默认含义就是将t1按成员逐个复制到t2。对于赋值的这种解释方式,在应用到具有指针成员的类的对象时,就可能产生一种出人意料的(通常也是人们所不希望)的作用。对于包含了由构造函数/析构函数管理的资理的对象而言,按成员复制的主义通常是不正确的。例如,
void h()
{
Table t1;
Table t2 = t1; // 复制初始化:麻烦
Table t3;
T3 = t2; // 复制赋值:麻烦
}
在这里,Table的默认构造函数为t1和t3各调用了一次,一共两次。然而Table的析构函数则被调用了3次:对t1、t2和t3各一次!由于赋值的默认解释是按成员赋值,所以,在h()结束时,t1、t2和t3中将各包含一个指针,它们都指向建立t1时从自由存储中分配的那个名字数组。在建立t3时所分配的数组的指针并没有保留下来,因为它被赋值t3 = t2覆盖掉了。这样,如果没有自动废料收集,对这个程序而言,该数组的存储就将永远丢掉了。而在另一方面,为t1的创建而分配的数组因为同时出现在t1、t2和t3里,将被删除3次。这种情况所导致的结果是无定义的,很可能是灾难性的。
这类反常情况可以避免,方式就是将Table复制的意义定义清楚:
Table(const Table&); // 复制构造函数
Table& operator=(const Table&); // 复制赋值
程序员可以为这些复制操作定义自己认为最合适的任何意义,但对于这类容器,传统上就是复制那些被包含的元素(或至少是让用户以为做了复制)。例如,
Table::Table(const Table& t) // 复制构造函数
{
p = new Name[sz = t.sz];
for (int i = 0; i<sz; i++) p[i] = t.p[i];
}
Table& Table::operator=(const Table& t)
{
if (this != &t) { // 当心自赋值:t = t
delete[] p;
p = new Name[sz = t.sz];
for (int i = 0; i<sz; i++) p[i] = t.p[i];
}
return *this;
}
情况几乎总是如此,复制构造函数与复制赋值通常都不一样。究其根本原因,复制构造函数是去完成对未初始化的存储区的初始化,而复制赋值运算符则必须正确处理一个结构良好的对象。
在某此情况下,可以对赋值做一些优化,但赋值运算符的一般性策略非常简单:防止自赋值,删除那些老元素,初始化,复制那些新元素。通常每个非静态的成员都必须复制。可以通过异常去报告复制中出错的情况。
参考资料:《C++程序设计语言(特别版)》