在类的设计中,析构函数,拷贝构造函数,重载赋值操作运算符并称为C++的三大构造函数,也是这三大函数将C++的独有特性体现的淋漓尽致,然而很多人在类的设计中有可能忽略其重要性。有时候,忽略了可能并没有造成影响,因为编译器都会为其加载默认版本的三大函数,而正好默认函数足以满足其需求。
一、什么时候需要显式定义拷贝构造函数?
- 一种最简单的判别方法,如果成员变量中有需要动态分配内存时,需要显式定义拷贝构造函数。
成员变量中动态分配了内存,那么必定需要再析构函数中回收,因此我们需要显示定义析构函数回收内存空间。指针变量在拷贝的过程中,默认的拷贝构造函数只会拷贝指针的地址,不会拷贝指针所指向的内容,那么拷贝的对象与原对象的指针指向同一数据块,如果原对象的数据发生变化,那么拷贝的对象的数据也发生了变化,因为实际上他们是同一个东西。因此需要显式定义拷贝构造函数,要显式定义拷贝构造函数往往就需要显式定义拷贝构造函数来实现拷贝过程。
举例分析
1 #include <iostream> 2 class test 3 { 4 public: 5 test(int init = 0) 6 { 7 ch = new int(init); 8 } 9 10 ~test() 11 { 12 delete ch; 13 } 14 void setCh(int rth) 15 { 16 *ch = rth; 17 } 18 19 int getCh() const 20 { 21 return *ch; 22 } 23 private: 24 int* ch; 25 }; 26 27 int main() 28 { 29 test origin; //原始对象,默认构造函数初始化 30 test copy = origin; //拷贝对象 31 std::cout << origin.getCh() << " " << copy.getCh() << std::endl; 32 33 //改变原始对象的值 34 origin.setCh(1); 35 std::cout << origin.getCh() << " " << copy.getCh() << std::endl; 36 37 system("pause"); 38 }
运行结果
显然这并不是我们希望看到的结果,我们希望origin对象与copy对象能独立的存在,因此这种情况下我们需要显示定义拷贝构造函数,如下所示:
1 //拷贝构造函数 2 test(const test & T) 3 { 4 ch = new int; 5 *ch = *T.ch; 6 }
加入拷贝构造函数后,运行结果,符合我们的预期
因为举例类的结构比较简单,因此在拷贝构造函数中就一次性完成了深层次的拷贝,当然我们也可以通过重载运算符和拷贝构造函数实现。
1 //拷贝构造函数 2 test(const test & T) 3 { 4 ch = new int; 5 *this = T; 6 //上一行也可以等价的写成 7 /*this->operator=(T);*/ 8 } 9 10 //重载赋值运算符 11 test& operator = (const test & T) 12 { 13 *ch = *T.ch; 14 return *this; 15 }
根据上述代码中的第7行,可以看出赋值运算符实际是一个函数,用来给拷贝构造函数调用。
- 要阻止对象进行拷贝时,需要显式定义拷贝构造函数
对于有些类,生成对象后,我们不希望其被拷贝,有两种方式实现阻止拷贝的发生。
1、根据C++11的特性,将拷贝构造函数和拷贝赋值运算符定义为删除的,可以阻止拷贝。
1 test(const test & T) = delete; //阻止拷贝 2 test& operator = (const test & T) = delete; //阻止赋值
2、将拷贝构造函数和拷贝赋值运算符定义为私有的,这样外部类无法访问
- 成员变量中存在const的类型的,需要显式定义拷贝构造函数。
这个可以很容易理解,因为const类型的只能在初始化器中初始化,通过拷贝来初始化必然会出错。
需要显式定义的情况还有很多,后面遇到了在补充,但总归一点,尽量定义自己的拷贝构造函数,防止任何可能的错误发生
二、怎样定义拷贝构造函数
怎样定义其实上述代码已经基本给出,拷贝构造函数的参数是一个常量引用,需要重新动态分配内存空间的先分配,然后进行深层次的拷贝。合成的拷贝构造函数会出问题主要是由于浅层次拷贝的发生。