Rule of Three, 复制控制
作者:Jason Lee @http://blog.csdn.net/jasonblog
日期:2010-04-13
[1]复制构造函数copy constructor
Rule of Three是指类如果需要析构函数,则通常也需要复制构造函数和赋值操作符。而其实习惯地显示编写这三者本就是一个良好的习惯。因为相较于编译器自动生成的代码,显示编写自己的代码能让程序员对整个程序有着更清晰的认识和把握。
从形式上说,复制构造函数具有单个本类型对象引用的形参(通常用const限定修饰),并且函数名与类名相同,因为复制构造函数也是构造函数。比如:
Demo(const Demo &demo){}
复制构造函数从需要上来看有两种情况,一是有成员在构造函数中分配资源,比如指针 是最典型的例子;二是需要在复制过程做特定工作。
从作用场合来谈,本质上可以看做是至少存在两个对象的情况下需要复制构造函数,因为需要使用其一来对其它对象进行初始化。比如:
1. 根据同类型对象初始化某一对象;我个人觉得在这种情况下,很容易疏忽的一个细节之处是:一般,对构造函数比较生疏的程序员习惯地定义一个默认构造函数后,并且在里面进行指针所需空间的开辟和分配,就会潜意识地以为以后每个该类对象初始化时都会合理地首先对指针成员进行内存分配,然而却没有足够深刻地意识到如果调用了复制构造函数,则不会调用默认函数:
#include <iostream> using namespace std; class Demo { public: Demo(){ p = new int(1); } Demo(const Demo &demo){ *p = *demo.p; }//指针p并未被分配空间 ~Demo(){} void setP(int v){ *p = v; } void show(){ cout << *p << endl; } private: int *p; }; int main(){//程序运行时出错 Demo d1; d1.setP(3); Demo d2 = d1;//只调用复制构造函数,并没有调用默认构造函数,所以指针未拥有空间 return 0; }
2. 将对象作为普通实参;这种情况最容易与值传递的情况进行联想:传递的是与变量等值的一个临时值,而非同一个变量。同样的,以对象作为普通实参,也需要建立一个临时对象,就需要调用到复制构造函数。比如以下一段毫无意义的代码:
3. 从函数中返回一个普通对象;这种情况的道理与上一种情况一样,两种情况可以出现在一段代码中:
Demo Test(Demo demo){ Demo temp; return temp; }
4. 先调用构造函数创建一个临时对象,再调用复制构造函数将该临时对象复制到容器中的每个元素;
#include <iostream> #include <vector> using namespace std; class Demo { public: Demo(){ p = new int(1); } Demo(int v){ p = new int; *p = v; } Demo(const Demo &demo){ p = new int; *p = *demo.p; } ~Demo(){} void setP(int v){ *p = v; } void show(){ cout << *p << endl; } private: int *p; }; int main(){ vector<Demo> demos(3);//首先调用Demo()这个默认构造函数,然后再根据建立的临时对象调用复制构造函数初始化其余元素 return 0; }
5. 使用花括号对数组进行初始化时。
#include <iostream> using namespace std; class Demo { public: Demo(){ p = new int(1); } Demo(int v){ p = new int; *p = v; } Demo(const Demo &demo){ p = new int; *p = *demo.p; } ~Demo(){} void setP(int v){ *p = v; } void show(){ cout << *p << endl; } private: int *p; }; int main(){ Demo demos[3] = { Demo(3) };//只根据临时对象初始化demos[0] demos[0].show(); return 0; }
值得记录的是当类具有指针成员时,如果没有显示编写复制构造函数,而使用编译器自动合成的复制构造函数,就很容易出现“指针悬挂”问题。而其实并不只是该问题,当多对象运作时,由于使用了编译器自动合成的复制构造函数,导致成员指针具有相同的地址,这就有点类似静态成员了,实际上多对象操作的都是同一片内存空间。
[2] 赋值操作符assignment operator
赋值操作符是重载运算符的一种。相较于复制构造函数出现于用于初始化对象的场合,赋值操作符则出现于使用对象进行赋值的场合。实际上,二者通常同时需要,甚至可以看做一体。
#include <iostream> using namespace std; class Demo { public: Demo(){ p = new int(1); } ~Demo(){} void setP(int v){ *p = v; } void show(){ cout << *p << endl; } Demo& operator=(Demo &demo){ *p = *demo.p; return *this; } private: int *p; }; int main(){ Demo d1,d2; d1.setP(3); d2 = d1; d2.show(); return 0; }
[3] 析构函数destructor
析构函数按照成员在类中的声明次序逆序撤销成员,这与构造函数初始化列表又发生了关系。通常,普通对象在超出作用域时会调用析构函数,比如一个函数中的局部作用域对象;另外一种情况则是删除指向对象的指针,会调用对象的析构函数。如果指针是指向一个STL 容器或者内置数组,且容器或数组的单元类型是对象的话,也会调用对象的析构函数,并且容器或数组中的元素是按逆序撤销的。
#include <iostream> using namespace std; class Demo { public: Demo(){ p = new int(1); } Demo(const Demo &demo){ p = new int; *p = *demo.p; } ~Demo(){ delete p; } void setP(int v){ *p = v; } void show(){ cout << *p << endl; } Demo& operator=(Demo &demo){ *p = *demo.p; return *this; } private: int *p; }; int main(){ Demo d1,d2;//调用默认构造函数 d1.setP(3); Demo d3 = d1;//调用复制构造函数 d2 = d1;//使用赋值操作符 //析构... return 0; }