复制控制这一节需要注意的地方不多,主要有以下几点:
1、定义自己的复制构造函数
什么时候需要定义自己的复制构造函数,而不用系统提供的,主要遵循以下的经验说明:
某些类必须对复制对象时发生的事情加以控制,这样的类(1)经常有一个数据成员是指针,(2)有成员在构造函数中分配的其他资源;
而另一些类在创建对象时必须做一些特定的工作。
2、禁止复制
有些类是需要禁止复制的,如iostream类就不允许复制,但编译器始终都会默认合成一个,但还是有办法的:
为了防止复制,类只要显示声明其复制构造函数为private就行了。
然而,这样,类的友元和成员仍可以进行复制,如果想要连友元和成员中的复制也禁止,就可以声明一个private复制构造函数但不对其定义,这是合法的。
3、析构函数的异同
与复制构造函数或赋值操作符不同,编译器总是会为我们合成一个析构函数,合成析构函数按照对象创建时的逆序撤销每个非static成员。
析构函数与复制构造函数或赋值操作符之间的一个重要区别是:即使我们编写了自己的析构函数,合成析构函数仍然会运行。合成析构函数。
4、智能指针
智能指针是由于在有指针成员的类中,指针所指向的对象是共享的,防止出现悬垂指针而提出的一种管理指针的办法。
为了阐述智能指针,我们来看一个例子:
1 class HasPtr{ 2 public: 3 HasPtr(int *p, int i):uptr(p), val(i) {} //p是指向int型数组的指针 4 HasPtr& operator = (const HasPtr& rhs); 5 int *getPtr() { 6 return uptr->ip; 7 } 8 int getValue() { 9 return val; 10 } 11 12 void setPtr(int *p) { 13 uptr->ip = p; 14 } 15 void setValue(int i) { 16 val = i; 17 } 18 19 int getPtrValue() const { 20 return *uptr->ip; 21 } 22 void setPtrValue(int i) { 23 *uptr->ip = i; 24 } 25 26 private: 27 int *uptr; 28 int val; 29 };
如上一个类,如果我像这样调用:
int obj = 0;
HasPtr ptr1(&obj, 42);
HasPtr ptr2(ptr1);
ptr1和ptr2的值相同,改变任意一个的值都可以改变其共享对象的值。
再看,可能出现悬垂指针的情况:
int *ip = new int(42);
HasPtr ptr(ip, 10);
delete ip;
ptr.set_ptr_val(0); //Disaster!!!
这里ip和ptr中的指针指向了同一对象,删除了该对象时,ptr中的指针不再指向有效对象,但是你又不知道该对象不在了,所以,这样就出现了悬垂指针。
所以,定义智能指针能有效地解决这个问题,为了避免多个指针共享一个对象时撤销出现的悬垂指针问题,定义智能指针类的主要功能就是来保证在撤销指向对象的最后一个指针时才删除该对象。
为了统计指向共享对象的指针的数量,引入使用计数,用其跟踪该类有多少个对象共享同一指针,但使用计数为0时,删除对象。在设计上,将使用计数设计成一个单独的类,用来封装使用计数和相关指针。
如下:
1 //仅由HasPtr使用的U_Ptr类,用于封装使用计数和相关指针 2 class U_Ptr { 3 friend class HasPtr; //定义成友元 4 size_t use; 5 int *ip; 6 U_Ptr(int *p):ip(p), use(1) {} 7 ~U_Ptr() { delete ip; } 8 };
引用上面的那个类,不同的是,让HasPtr类保存一个指向U_Ptr对象的指针,U_Ptr对象再指向实际的int基础对象。如下:
1 class HasPtr{ 2 public: 3 HasPtr(int *p, int i):uptr(new U_Ptr(p)), val(i) {} //p是指向int型数组的指针 4 HasPtr(const HasPtr& orig):uptr(orig.uptr),val(orig.val) { 5 ++uptr->use; //复制完成将使用计数加1 6 } 7 HasPtr& operator = (const HasPtr& rhs); 8 ~HasPtr() { 9 if(--uptr->use == 0) //检查假如只有一个对象在共享该指针,则删除 10 delete uptr; 11 } 12 13 int *getPtr() { 14 return uptr->ip; 15 } 16 int getValue() { 17 return val; 18 } 19 20 void setPtr(int *p) { 21 uptr->ip = p; 22 } 23 void setValue(int i) { 24 val = i; 25 } 26 27 int getPtrValue() const { 28 return *uptr->ip; 29 } 30 void setPtrValue(int i) { 31 *uptr->ip = i; 32 } 33 34 private: 35 U_Ptr *uptr; 36 int val; 37 };
其中,红色部分是改动过的。赋值操作符像下面这样:
1 HasPtr& HasPtr::operator =(const HasPtr &rhs) 2 { 3 ++ rhs.uptr->use; 4 if (--uptr->use == 0) 5 delete uptr; 6 uptr = rhs.uptr; 7 val = rhs.val; 8 return *this; 9 }
还有一种方法是定义值类型:
这种思路很简单,就是给指针成员提供值语义,复制值型对象时,会得到一个不同的新副本,对副本所做的改变不会反映在原有对象上。如下,可以对赋值操作符做点改变:
1 HasPtr& HasPtr::operator =(const HasPtr &rhs) 2 { 3 *uptr = *rhs.uptr; 4 val = rhs.val; 5 return *this; 6 }