为什么当类的成员有指针时,需要特别注意?
因为当一个指针复制给另一个指针时,两个指针指向同一个对象,使用二者之一修改指向的对象,更特殊的,如果使用一个指针删除了对象时,另一个指针还以为对象存在,这就会造成很大的灾难。
通常有3种方法管理指针成员:
1.常规管理。就是将一个指针复制给另一个指针,这样做的好处是不需要额外定义复制控制,但是会出现前面所提到的问题。
2.智能指针。虽然这些指针共享同一个数据,利用一个数据结构记录了有多少指针指向这个数据。当只剩下一个指针指向这个数据时,才能进行删除这个数据的操作,这样可以避免悬垂指针。
3.定义值型类。此时指针并不直接指向对象,而是创建一个对象的副本,指针唯一的指向这个副本。这样这些指针就不再“耦合”,通过指针对对象的修改、删除只能影响各自的副本。
下面分别介绍这3种情况,先看第一种情况。
通过一个类来说明这种直接复制的特点:
class HasPtr
{
public:
//构造函数
HasPtr(int *p,int i):ptr(p),val(i){}
//get函数
int *get_ptr()const{return ptr;}
int get_int() const
{
std::cout<<"val = "<<val<<std::endl;
return val;
}
//set函数
void set_ptr(int *p){ptr = p;}
void set_int(int i){val = i;}
//get和set指针指向的对象
int get_ptr_val()const
{
std::cout<<"ptr point to "<<*ptr<<std::endl;
return *ptr;}
void set_ptr_val(int val)const {*ptr = val;}
private:
int *ptr;
int val;
};
这个类有两个数据成员,一个int的值,一个int*的指针。它俩之间并没有什么关系(并不是我们美好想象的那样:指针指向这个值)。
然后定义了一些简单的set、get函数。
通过下面的程序就能看出它们的关系了:
int main()
{
int a = 0;
int *p = &a;
HasPtr ptr1(p,a);
cout<<"value = "<<ptr1.get_int()<<endl;
cout<<"point is point to "<<ptr1.get_ptr_val()<<endl;
//数据和指针不相关
ptr1.set_int(1);
cout<<"value = "<<ptr1.get_int()<<endl;
cout<<"point is point to "<<ptr1.get_ptr_val()<<endl;
//用ptr1初始化ptr2
HasPtr ptr2(ptr1);
//值已被修改为1
cout<<"value = "<<ptr2.get_int()<<endl;
//指针未被修改,依然指向0
cout<<"point is point to "<<ptr2.get_ptr_val()<<endl;
a = 10;
cout<<"value = "<<ptr2.get_int()<<endl;
//指针指向的内容发生变化
cout<<"point is point to "<<ptr2.get_ptr_val()<<endl;
int *ip = new int(42); //动态分配值
HasPtr ptr3(ip,42);
HasPtr ptr4(ptr3);
delete ip; //删除指针
ptr3.set_ptr_val(0); //发生灾难
return 0;
}
那就是它们的数据时无关的,而指针却是指向同一个对象的,修改一个指针指向的内容会影响另一个。
接下来我们看看智能指针。
智能指针希望达到的效果是:复制行为跟前面的类似:它的复制对象时,副本和源对象的指针指向相同。改变其中的一个会是另一个也改变。但是在在构函数中删除指针时,不会无条件的删除指针导致出现前面的空指针情况,而是判断是否有其他指针也指向同一个基础对象,如果有的话,就不能删除它,直到没有其他对象的指针指向这个基础对象了,才能删除它。
如何实现这个功能呢?说起来很简单,设置一个“引用计数”(或者叫“使用计数”)。有多少指针共享一个对象,就记录为几,只有当所有的指针都被删除了,才会删除对象。
具体的说,当创建一个新对象时,初始化指针并使引用计数为1。当这个对象作为另一个对象的副本创建时,引用计数加1;如果是赋值操作时,左操作数的引用计数减少1,而右操作的引用计数增加1。调用析构函数时,会减少引用计数的值,直到引用计数减为0,才会删除基础对象。
说起来容易做起来难,首先要解决的问题就是引用计数放到哪里。其中的一种解决方案是单独定义一个引用计数类来封装引用计数以及相关的指针。我们先看程序:
//类的声明
class HasPtr;
class U_Ptr
{
//声明友元类:HasPtr可以访问本类的私有函数
friend class HasPtr;
//原来HasPtr类的数据成员:指针,变成了U_Ptr类的成员
int *ip;
//引用计数
size_t use;
//构造函数
U_Ptr(int *p):ip(p),use(1){}
//析构函数:删除指针
~U_Ptr(){delete ip;}
};
注意,这个类的所有成员都是私有的,普通用户并不能访问。我们将这个类设为HasPtr的友元,这样HasPtr就能访问这个类的成员。这个类定义了指针ip取代了原来HasPtr中的指针,也定义了引用计数。它的构造函数接受一个指向int的指针,用它来初始化自己的指针,而引用计数初始化为1。析构函数删除这个指针。
而原来的HasPtr类定义如下:
class HasPtr
{
public:
//构造函数:
HasPtr(int *p,int i):ptr(new U_Ptr(p)),val(i){}
//复制构造函数
HasPtr(const HasPtr &orig):ptr(orig.ptr),val(orig.val){++ptr->use;}
//赋值操作
HasPtr& operator=(const HasPtr&);
//析构函数
~HasPtr(){if(--ptr->use == 0)delete ptr;}
//set和get函数
//里面与指针有关的多变成访问ptr里面的ip指针
int *get_ptr()const{return ptr->ip;}
int get_int()const {return val;}
void set_ptr(int *p){ptr->ip = p;}
void set_int(int i){val = i;}
int get_ptr_val()const {return *ptr->ip;}
void set_ptr_val(int i){*ptr->ip = i;}
private:
//引用计数类对象
U_Ptr *ptr;
//值
int val;
};
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
++rhs.ptr->use;
if(--ptr->use == 0)
delete ptr;
ptr = rhs.ptr;
val = rhs.val;
return *this;
}
私有成员的int型指针变成了指向U_Ptr 的类型。构造函数的输入是一个int值来初始化自己的值,在初始化自己的U_Ptr指针时,动态分配了一个U_Ptr类,调用这个类的构造函数来初始化它。复制构造函数中,值和指针的复制都是直接的,但是复制以后引用计数会加1。赋值构造函数有点复杂,它是得赋值的有操作数(rhs)的引用计数加1,左操作数的引用计数减1(如果减到了0,删除左操作数的指针)。然后用右操作数的指针和值复制给左操作数的指针和值。析构函数要判断指向某个数的指针是否为0,如果是,则删除这个指针。而在set和get函数中,只用把原来直接获取指针变为获得U_Ptr类中的指针就行了。
最后我们看看定义值型类。
首先这个名字就很奇怪。需要仔细解释一下:“值型类”意味着定义的类跟值很像。而我们知道,当复制值时,复制的不是值本身,而是值的一个副本。我们希望创建的类也有这样的特征,这样的话,虽然指针不能共享数据,但是它从根本上避免了指针的纠缠。先看一个例子:
class HasPtr
{
public:
//构造函数
HasPtr(const int &p,int i):ptr(new int(p)),val(i) {}
//复制构造函数
HasPtr(const HasPtr &orig):ptr(new int (*orig.ptr)),val(orig.val){}
//复制操作符
HasPtr& operator=(const HasPtr&);
//析构函数
~HasPtr(){delete ptr;}
int get_ptr_vaule()const {return *ptr;}
int get_int()const{return val;}
void set_ptr(int *p){ptr = p;}
void set_ptr_val(int i){val = i;}
int *get_ptr()const {return ptr;}
void set_ptr_val(int p)const{*ptr = p;}
private:
int *ptr;
int val;
};
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
*ptr = *rhs.ptr;
val = rhs.val;
return *this;
}
可以发现,对于构造函数和复制构于造函数,对于实参指针,采取的办法都是建立一个一个新的指针,让新指针的指向实参相同,然后用它来初始化数据成员中的指针。在赋值操作符中,我们并没有将某个地址给指针,而是让指针指向一个具体的值,这个值是赋值操作符的有操作数的指针指向的。这意味着进行复制操作以后,两个操作数的指针成员指向的值相同(而不是指向同一个对象!)最后,在析构函数中,需要删除这个指针