【1】认识智能指针
什么是智能指针?根据面向对象编程的思想,智能指针肯定也是一种对象。
好吧!那么,既然是对象,肯定就有对应的类。而所构建智能指针对象的类即就是它的核心内容。
简而言之,所构建智能指针对象的类是有且仅有一个指针成员变量的类。
智能指针的潜规则就是所谓的这个唯一的指针成员变量肯定指向一个在堆上创建的对象。
所以,智能指针本质是存储动态分配(堆上创建)对象指针的类所构建的对象(下面称堆上所创建的那个对象为基础对象)。
主要用于基础对象的生存期控制,能够确保自动正确的销毁基础对象(动态分配的对象),防止内存泄露。
它的一种通用实现技术是使用引用计数(reference count)。
即就是智能指针类将一个计数器与类指向的基础对象彼此关联,引用计数跟踪该类有多少个对象共享同一指针(即就是指向那块内存)。
每次创建类的新对象时,初始化指针值并将引用计数默认先置为1。
当当前对象作为新对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数(每次加1)。
当对一个对象进行赋值时:
赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;
当调用析构函数时,析构函数判断引用计数(如果引用计数减至0,则删除基础对象),执行对应的操作。
【2】为什么我们需要智能指针?
当生成一个对象的时候,如果这个类很大,这个对象会占用很多空间。
那么每生成一个对象就需要占用一片空间,这样会占用很多系统资源。同时降低效率。
一个解决方法就是当用拷贝构造函数新创建一个的对象时:
让它不存储数据,而只存储一个指向原来对象数据的指针(其实也就是我们所说的浅拷贝)。 这样空间就节省了很多。
但问题在于,这样的话两个object完全联结在了一起。如果修改了其中一个,另一个也跟着变了。所以这种方法不可取。
那么,下面讲的技术就是解决这类问题的方法。
当通过一个已有对象通过拷贝构造创建新对象时,新object只有一个指向已有object的指针。这两个object共享数据。
直到其中一个需要修改数据的时候,再把这两块数据分离。
这里举一个最简化的例子:假设一个class叫 CLargeObject,里面存有很多数据。
我们用一个inner class来把所有数据放在一起,叫CData。CData里面存有大量数据,例如一个数据库。
这里用最简单的模型来表示,假设只有一个整数int m_nVal。
CData里面需要包含另一个变量,叫作索引数目(reference count)。
它记录了总共有多少CLargeObject的object正在引用着当前的CData object。
示例代码如下:
#include<iostream>
using namespace std;
class CLargeObject
{
public:
CLargeObject(int nVal);
CLargeObject(const CLargeObject & ob);
~CLargeObject();
CLargeObject & operator = (const CLargeObject &ob);
void SetVal(int nNewVal);
int GetVal()const;
private:
struct Data
{
public:
Data(int nVal):m_nVal(nVal),m_nReferenceCount(1)
{}
private:
friend class CLargeObject;
Data *get_own_copy()
{
if(m_nReferenceCount == 1)
return this;
m_nReferenceCount--;
return new Data(m_nVal);
}
int m_nReferenceCount;
int m_nVal;
};
Data *m_pData;
};
CLargeObject::CLargeObject(int nVal)
{
m_pData = new Data(nVal);
}
CLargeObject::CLargeObject(const CLargeObject &ob)
{
ob.m_pData->m_nReferenceCount++;
m_pData = ob.m_pData;
}
CLargeObject::~CLargeObject()
{
if(--m_pData->m_nReferenceCount == 0)
delete m_pData;
}
CLargeObject & CLargeObject::operator =(const CLargeObject &ob)
{
ob.m_pData->m_nReferenceCount++;
if(--m_pData->m_nReferenceCount == 0)
delete m_pData;
m_pData = ob.m_pData;
return *this;
}
void CLargeObject::SetVal(int nNewVal)
{
m_pData = m_pData->get_own_copy();
m_pData->m_nVal = nNewVal;
}
int CLargeObject::GetVal() const
{
return m_pData->m_nVal;
}
void main()
{
CLargeObject obj1(10);
CLargeObject obj2(obj1);
obj2.SetVal(22);
cout<<obj1.GetVal()<<endl; //10
cout<<obj2.GetVal()<<endl; //22
}
36~39行:构造函数,建立堆上的对象
40~44行:拷贝构造函数,仅仅引用计数加1,通过赋值语句完成数据共享
50~57行:赋值函数
CLargeObject & CLargeObject::operator =(const CLargeObject &ob)
{
ob.m_pData->m_nReferenceCount++;
if(--m_pData->m_nReferenceCount == 0)
delete m_pData;
m_pData = ob.m_pData;
return *this;
}
深度分析:
<1>为了防止自身赋值(即就是如果两个对象相同),
在对左对象(或者左操作数)的使用计数操作之前使原对象的使用计数先加1
若恰相等,if语句必进行减1操作,意味着计数前后没有改变,达到目的
若不相等,意味着原对象计数加1,被赋值对象计数减1,也成立
然后相应成员进行赋值
<2>如果左对象的使用计数减至0,那么赋值操作符必须删除它之前所指向的对象,
再进行相应成员赋值
45~49行:析构函数,通过计数判断是否为最后一次删除。若是,摧毁共享数据;若非,仅仅计数减1,达到目的
58~62行:设置值函数,可以看出其中调用了函数get_own_copy()
Data *get_own_copy()
{
if(m_nReferenceCount == 1)
return this;
m_nReferenceCount--;
return new Data(m_nVal);
}
60行:m_pData = m_pData->get_own_copy();
左操作数的指针成员(m_pData)很关键。
若引用计数为1,说明该共享数据仅仅被引用了一次,那么,修改值就仅仅需要在原内存中进行更改,所以,返回this指针,即本身指针
若引用计数大于1,说明该共享数据不仅被一个对象引用。那么,修改该值的就要统筹兼顾,顾全大局,步骤如下:
首先:保证原数据块依然存在
另外:使引用计数减1
最后:建立一个新的数据块
函数结束,返回新数据块的指针赋值给m_pData
即就是实现了所谓的分离
61行:m_pData->m_nVal
= nNewVal;
通过指针实现新数据的输入。
63~66行:取值函数。之所以定义为常函数是因为防止在函数内部m_pData被修改。
【3】坊间流传部分代码(整理篇)
#include<iostream>
using namespace std;
class U_Ptr
{
friend class HasPtr; //友元类
int *ip;
size_t use; //计数器
U_Ptr(int *p=NULL):ip(p),use(1)
{
}
~U_Ptr()
{
delete ip;
cout<<"U_Ptr"<<this<<endl;
ip = NULL;
}
};
class HasPtr
{
private:
U_Ptr *ptr;
int val;
public:
////////////////////构造函数
HasPtr(int *ip = NULL, int i = 0):ptr(new U_Ptr(ip)),val(i)
{
cout<<"construct"<<this<<endl;
}
///////////////////拷贝构造函数
HasPtr(const HasPtr & orig):ptr(orig.ptr),val(orig.val)
{
cout<<"copy construct"<<this<<endl;
++ptr->use;
}
////////////////赋值函数
//赋值意味着左操作对象已经存在
//赋值分为自身赋值和非自身赋值
//自身赋值:尽量避免。若此例中自身赋值,分析如下:
//首先,各自计数均加1;其次,左操作对象计数减1,若为0,则delete;否则对应成员赋值。
//非自身赋值:一般必做判断if(this != &rhs)此例中分析如下:
//首先,有操作对象计数加1;其次,左操作对象计数减1,若为0,则delete;否则对应成员赋值。
HasPtr & operator =(const HasPtr &rhs)
{
cout<<"operator = "<<this<<endl;
++rhs.ptr->use; //右操作数的计数加1
if(--ptr->use == 0) //如果左操作数的引用计数减为0,则执行delete
{
delete ptr;
cout<<"operator = ptr->use == 0"<<" "<<this<<endl;
}
ptr = rhs.ptr;
val = rhs.val;
return *this;
}
//防止自身赋值
//如果是自身赋值,那么意味着引用计数应该不会改变,仅仅只是赋值
//如果非自身赋值,那么右操作数的计数加1。左操作数的计数减1,若减后为0,则执行delete.
///////////////析构函数
~HasPtr()
{
if(--ptr->use == 0)
{
delete ptr;
cout<<"delete ptr"<<" "<<this<<endl;
}
cout<<"~ HasPtr()"<<this<<" "<<this->ptr->use<<endl;
ptr = NULL;
}
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;
}
};
void main()
{
int *p = new int(1);
HasPtr obj1(p,100);
cout<<obj1.get_int()<<endl;
cout<<obj1.get_ptr()<<endl;
cout<<obj1.get_ptr_val()<<endl;
HasPtr obj2(obj1); //拷贝构造函数
cout<<obj2.get_int()<<endl;
cout<<obj2.get_ptr()<<endl;
cout<<obj2.get_ptr_val()<<endl;
HasPtr obj3 = obj1; //拷贝构造函数
cout<<obj3.get_int()<<endl;
cout<<obj3.get_ptr()<<endl;
cout<<obj3.get_ptr_val()<<endl;
HasPtr obj4;
obj4 = obj1; //赋值函数
cout<<obj4.get_int()<<endl;
cout<<obj4.get_ptr()<<endl;
cout<<obj4.get_ptr_val()<<endl;
obj4 = obj4; //自身赋值语句 检测赋值函数作用
int *s = new int(20);
cout<<s<<endl;
obj1.set_ptr(s);
cout<<obj1.get_ptr()<<endl;
cout<<obj2.get_ptr()<<endl;
cout<<obj3.get_ptr()<<endl;
obj1.set_int(99);
obj2.set_int(88);
obj3.set_int(77);
cout<<obj1.get_int()<<endl;
cout<<obj2.get_int()<<endl;
cout<<obj3.get_int()<<endl;
obj1.set_ptr_val(66);
obj1.set_ptr_val(55);
obj1.set_ptr_val(44);
cout<<obj1.get_ptr_val()<<endl;
cout<<obj2.get_ptr_val()<<endl;
cout<<obj3.get_ptr_val()<<endl;
}
/*
*运行结果:
construct003EFEF0
100
00724B48
1
copy construct003EFEE0
100
00724B48
1
copy construct003EFED0
100
00724B48
1
construct003EFEC0
operator = 003EFEC0
U_Ptr00724C20
operator = ptr->use == 0 003EFEC0
100
00724B48
1
operator = 003EFEC0
00724C20
00724C20
00724C20
00724C20
99
88
77
44
44
44
~ HasPtr()003EFEC0 3
~ HasPtr()003EFED0 2
~ HasPtr()003EFEE0 1
U_Ptr00724B88
delete ptr 003EFEF0
~ HasPtr()003EFEF0 4277075694
*/
分析过程:
第一:需要定义一个单独的具体类用以封装使用计数和相关指针,
把U_ptr类的所有成员定义为private,因为我们不希望普通用户使用U_ptr类
第二:将HasPtr类设置为其友元
第三:每个HasPtr对象将指向一个U_Ptr对象,使用计数机制跟踪指向每个U_Ptr对象的HasPtr对象的数目。
第四:U_Ptr类定义的仅有函数为构造函数和析构函数,构造函数复制指针,并且将使用计数设置为1,表示一个HasPtr对象指向这个U_Ptr对象,析构函数释放指针
第五:HasPtr类的赋值函数很经典:
1>为了防止自身赋值,在减少左操作数的使用计数之前使rhs的使用计数加1
即就是:如果左右操作数相同,赋值操作符的效果将是U_Ptr基础对象的使用计数加一之后立即减一
2>如果左操作数的使用计数减至0,那么赋值操作符必须删除它所指向的对象