我曾经小觑复制构造函数,认为它简单得和赋值一般,今天看完沉思录第四章,我沉默了。我现在只能说,它确实充满了太多我不曾了解的东西。
首先,我们需要知道的是,我们什么时候需要写一个复制构造函数呢?或许有人会说,不用写,不写不是也照样可以执行直接初始化么。对的,这是可行的。我们可以通过一段代码来观察这个现象:
1 class Test 2 { 3 private: 4 protected: 5 public: 6 int _num; 7 8 Test() 9 { 10 this->_num = 0; 11 } 12 13 Test(int num) 14 { 15 this->_num = num; 16 } 17 18 Test(const Test &ts) 19 { 20 this->_num = ts._num; 21 cout << "copy" << endl; 22 } 23 24 ~Test() 25 { 26 27 } 28 };
我们可以发现,上面的代码中,我并未定义复制构造函数,且为了测试方便,我将_num置为公有成员。然后我们对Test类执行下面的操作:
1 int main() 2 { 3 Test ts(10); 4 Test ts1(ts); 5 cout << ts._num << endl; 6 cout << ts1._num << endl; 7 8 return 0; 9 }
运行之后,我们发现ts对象与ts1对象的_num值是相同的。如此,好像确实是不用写复制构造函数也是可行的。那我们是不是真可以放弃复制构造函数了呢?下面,我们可以再看一个例子:
1 class Test 2 { 3 private: 4 protected: 5 public: 6 int _num; 7 static int _count; 8 9 Test() 10 { 11 this->_num = 0; 12 Test :: _count++; 13 } 14 15 Test(int num) 16 { 17 this->_num = num; 18 } 19 20 Test(const Test &ts) 21 { 22 this->_num = ts._num; 23 cout << "copy" << endl; 24 } 25 26 ~Test() 27 { 28 Test :: _count--; 29 } 30 }; 31 32 int Test :: _count = 0;
为了说明问题,我在原来Test类的基础上添加一个静态成员_count,旨在计数。下面,我们对Test类进行如下操作:
1 int main() 2 { 3 Test *ts = new Test(); 4 Test *ts1(ts); 5 6 delete ts; 7 delete ts1; 8 9 cout << Test :: _count << endl; 10 11 return 0; 12 }
运行之后,我们会发现_count竟然是-1!好了,不用奇怪,这就是默认的复制构造函数的缺陷!我们在设计类时,不写复制构造函数,则编译器会为我们自动生成一个默认的复制构造函数,而这个默认的复制构造函数,功能也是十分简单甚至可以说是简陋的!它只能保证将类数据成员做一一简单复制其值,对于静态数据成员,它可以说是束手无策的,它也不会去操作,这就造成了静态数据成员的遗漏。还有另一个问题,既然是简单的值复制,那假如我们写的类中包含一个或者多个将会进行动态分配空间的指针类型成员呢?那析构函数中必将使用delete或者delete[]吧,如此,在对象析构时,我们不难想到,那将是对同一内存空间的多次释放!
通过分析,我们了解了一些默认复制构造函数的弊端。下面我们再说浅复制,其实默认复制构造也算是一种浅复制,浅复制既是简单地将值一一复制,所以浅复制也将可能存在多次释放的问题,而类静态成员,其实我们可以通过显示写出复制构造函数来控制的,即使它属于浅复制。
关于多次释放的问题,我们则可以使用深复制来解决。假如类中有动态分配的指针类型的变量,浅复制的结果是,两个对象的成员指向了相同的位置,所以导致了多次释放。而深复制的做法则是,我们让其指向相同的值,而代表不同的地址。如下述代码:
1 class Rect 2 { 3 public: 4 Rect() // 构造函数,p指向堆中分配的一空间 5 { 6 p = new int(100); 7 } 8 Rect(const Rect& r) 9 { 10 width = r.width; 11 height = r.height; 12 p = new int; // 为新对象重新动态分配空间 13 *p = *(r.p); 14 } 15 ~Rect() // 析构函数,释放动态分配的空间 16 { 17 if(p != NULL) 18 { 19 delete p; 20 } 21 } 22 private: 23 int width; 24 int height; 25 int *p; // 一指针成员 26 };
这样我们就做到了避免了多次释放问题。下面我们再回到开头所说的问题,我们什么时候需要复制构造函数。从上面的情况看,当我们仅需要的是简单的复制值时,我们甚至可以不写,但是若涉及动态分配、静态成员等,我们就需要显式写出复制构造函数了(动态分配必须使用深复制,静态成员可深可浅)。