含有指针成员的类的拷贝
题目:下面是一个数组类的声明与实现。请分析这个类有什么问题,并针对存在的问题提出几种解决方案。
1 template<typename T> class Array 2 { 3 public: 4 Array(unsigned arraySize):data(0), size(arraySize) 5 { 6 if(size > 0) 7 data = new T[size]; 8 } 9 10 ~Array() 11 { 12 if(data) delete[] data; 13 } 14 15 void setValue(unsigned index, const T& value) 16 { 17 if(index < size) 18 data[index] = value; 19 } 20 21 T getValue(unsigned index) const 22 { 23 if(index < size) 24 return data[index]; 25 else 26 return T(); 27 } 28 29 private: 30 T* data; 31 unsigned size; 32 };
分析:我们注意在类的内部封装了用来存储数组数据的指针。软件存在的大部分问题通常都可以归结指针的不正确处理。
这个类只提供了一个构造函数,而没有定义构造拷贝函数和重载拷贝运算符函数。当这个类的用户按照下面的方式声明并实例化该类的一个实例
Array A(10);
Array B(A);
或者按照下面的方式把该类的一个实例赋值给另外一个实例
Array A(10);
Array B(10);
B=A;
编译器将调用其自动生成的构造拷贝函数或者拷贝运算符的重载函数。在编译器生成的缺省的构造拷贝函数和拷贝运算符的重载函数,对指针实行的是按位拷贝,仅仅只是拷贝指针的地址,而不会拷贝指针的内容。因此在执行完前面的代码之后,A.data和B.data指向的同一地址。当A或者B中任意一个结束其生命周期调用析构函数时,会删除data。由于他们的data指向的是同一个地方,两个实例的data都被删除了。但另外一个实例并不知道它的data已经被删除了,当企图再次用它的data的时候,程序就会不可避免地崩溃。
由于问题出现的根源是调用了编译器生成的缺省构造拷贝函数和拷贝运算符的重载函数。一个最简单的办法就是禁止使用这两个函数。于是我们可以把这两个函数声明为私有函数,如果类的用户企图调用这两个函数,将不能通过编译。实现的代码如下:
1 private: 2 Array(const Array& copy); 3 const Array& operator = (const Array& copy);
最初的代码存在问题是因为不同实例的data指向的同一地址,删除一个实例的data会把另外一个实例的data也同时删除。因此我们还可以让构造拷贝函数或者拷贝运算符的重载函数拷贝的不只是地址,而是数据。由于我们重新存储了一份数据,这样一个实例删除的时候,对另外一个实例没有影响。这种思路我们称之为深度拷贝。实现的代码如下:
1 public: 2 Array(const Array& copy):data(0), size(copy.size) 3 { 4 if(size > 0) 5 { 6 data = new T[size]; 7 for(int i = 0; i < size; ++ i) 8 setValue(i, copy.getValue(i)); 9 } 10 } 11 12 const Array& operator = (const Array& copy) 13 { 14 if(this == ©) 15 return *this; 16 17 if(data != NULL) 18 { 19 delete []data; 20 data = NULL; 21 } 22 23 size = copy.size; 24 if(size > 0) 25 { 26 data = new T[size]; 27 for(int i = 0; i < size; ++ i) 28 setValue(i, copy.getValue(i)); 29 } 30 }
为了防止有多个指针指向的数据被多次删除,我们还可以保存究竟有多少个指针指向该数据。只有当没有任何指针指向该数据的时候才可以被删除。这种思路通常被称之为引用计数技术。在构造函数中,引用计数初始化为1;每当把这个实例赋值给其他实例或者以参数传给其他实例的构造拷贝函数的时候,引用计数加1,因为这意味着又多了一个实例指向它的data;每次需要调用析构函数或者需要把data赋值为其他数据的时候,引用计数要减1,因为这意味着指向它的data的指针少了一个。当引用计数减少到0的时候,data已经没有任何实例指向它了,这个时候就可以安全地删除。实现的代码如下:
1 public: 2 Array(unsigned arraySize) 3 :data(0), size(arraySize), count(new unsigned int) 4 { 5 *count = 1; 6 if(size > 0) 7 data = new T[size]; 8 } 9 10 Array(const Array& copy) 11 : size(copy.size), data(copy.data), count(copy.count) 12 { 13 ++ (*count); 14 } 15 16 ~Array() 17 { 18 Release(); 19 } 20 21 const Array& operator = (const Array& copy) 22 { 23 if(data == copy.data) 24 return *this; 25 26 Release(); 27 28 data = copy.data; 29 size = copy.size; 30 count = copy.count; 31 ++(*count); 32 } 33 34 private: 35 void Release() 36 { 37 --(*count); 38 if(*count == 0) 39 { 40 if(data) 41 { 42 delete []data; 43 data = NULL; 44 } 45 46 delete count; 47 count = 0; 48 } 49 } 50 51 unsigned int *count;
转自何海涛博客