第五十四讲 引用计数(1)

嗯,本来说到的周末不推送的,但碍于前两天加班没来及更大家更新,所以趁着今天嘴上长了个溃疡,疼得要命,也就没有出去玩,就给大家说一下新东西吧。

引用计数这个词大家想必都听说过,就好比说我们使用的string库里面就用到了引用计数,而且我们使用的智能指针share_ptr也是用引用计数实现的,现在我们来看下面这个问题:
———————–

gameboard My_Board(10,10);


My_Board.SetPiece(6,6,10);


gameboard My_Boad2(My_Board);


My_Boad2.SetPiece(6,6,20);


cout<<My_Board.GetPieceAt(6,6)<<endl;


cout<<My_Boad2.GetPieceAt(6,6)<<endl;


————————-

这是我们第五十一讲里面的内容,我们说得是一个棋盘,如果大家记得清楚的话,我们并没有实现复制构造函数,所以可以这样写,但是得到的东西却非我们所愿,我们原本期望得到的档案是:
—————-
10
20

—————-

但是我们却得到了如下的结果:
—————-
20
20

—————

大家可能知道会是为什么了,因为我们访问的是同一块内存,所以只要我们修改了一个就相当于修改另一个,当然,这不是重点了,重点是,当我们退出程序的时候,程序debug了,为什么会这样,其实刚才我们说了,因为两对象共享一块内存,当程序结束时执行析构函数,但是第一个对象被回收,接着在析构第二对象时却内存不在了,自然就报错了。那么我们该怎么解决这个问题呢?大家可能第一想到的是智能指针share_ptr,但是现在得告诉大家一个不幸的消息,当然可能只是我自己的不行,VS2012至今却不支持share_ptr,所以我们想用却用不成,而auto_ptr却无法满足我们的要求。

不过,没关系,我们可以尝试这写一个类似的东西,因为我们都已经知道了原理,写起来不是手到擒来吗?

引用计数,也就是说我们有一块内存,有多个对象都想拥有他,不过大家都知道,一旦持有该内存的对象析构后,这块内存也就被回收了,也就是说内存里的东西被销毁,而此时,那些还看似持有他的对象却变成了鬼魂野鬼,后果不堪设想。如果我们这样来:当一个对象持有这块内存时我们让一个计数器+1,而同样,当一个对象销毁时,计数-1,当这个技术为0时,我们就可以摧毁这块内存,这样就会变得安全了。

我们假设这个引用计数的东西叫做My_count,我们的智能指针叫做SmPt,那么我们可以这样定义:
————————

class My_Count{

friend class SmPt;

//让SmPt成为My_Count的友元类,完成更好的封装

My_Count();



My_Count(const My_Count&

);



My_Count&

operator=(const My_Count );



~My_Count();


int *p;


};


————————–

目前来说,我们想起来的就是这些了,那么大家是不是要问,为什么我们要把这个引用计数的类和我们的智能指针分开呢?都说了,为了数据更好的封装,但另外还有一个要点,那就是
如果智能指针不和引用计数分开的话,那么他就必须知道跟他一起被绑定到同一对象的其他的所有位置,这样才能更新引用计数的数据,同样,我们不能把引用计数装到我们的gameboard里面,如果那样的话,我们就得要重新写这个类,合格的C++程序员是应该把一切能够分开的东西分开,当然我不知道自己算不算合格的,但是我想名家经验之谈总该是要借鉴的。

My_Count里面的数据都是私有的,除了友元类SmPt能够访问外,他就是一个黑匣子,这复合面向对象的设计思想。

这个类的实现很简单:
———————————-
My_Count::My_Count():p(new int(1))
{ }

My_Count::My_Count(const My_Cont&

r):
p(r.p)
{

++*p;


}

My_Count&

My_Count::My_Count(const My_Count&

r)
{

++*r.p

if(–*p == 0)

{

delete p;


}

p = r.p

erturn *this;


}

My_Count::~My_Count()
{

if(–*p==0)

delete p;


}
————————————

大家可能有些不好理解的地方是赋值操作符的实现,简单点说吧,就好比,我,你,我们两个人开始的时候一人持一块蛋糕,我们的蛋糕或许都和其他人一起正分享着,所以,当你准备蛋糕分享给我的时候,你的蛋糕的计数就又多加了一个,而我要接受你的蛋糕,我就得把我的扔掉,于是那块蛋糕就少了一个人分享(那就是我啦),所以对我开始的那块蛋糕的引用计数就得-1,当然,我们要考虑到一个问题,如果原本只有我一个人持有那块蛋糕,我不用的话那就直接销毁呗,那么我们怎么知道是不是只有我一个人呢,if(–*p == 0)就是判断这个问题的了,如果只有我一个人享受这块蛋糕的话,-1自然就是0了,所以我们直接delete掉,然后再享受你的蛋糕,这么说不会还不明白了。

好吧,My_Count基本可以拿出来用了,我们现在怎么实现智能指针呢?首先我们同样想到的是构造函数和析构函数等4个函数,我们可以简单的先写出来,我们将他实现为模板类:
————————————
template<typename T>
class SmPt{
public:

SmPt();



SmPt(const SmPt&

);



SmPt&

opretor=(const SmPt&

);



virtrual ~SmPt();


private:

T *point;



My_Count m_Count;


};


————————————-
到现在,我们觉得好像少了些什么,首先,我们这个智能指针说到底是一种辅助,所以,我们我们应该可以通过我们想要的类型来构造这个对象:
——————-
Smpt(const T&

);


——————-

有了这个构造函数,我们在使用的时候就可以这样使:
——————
SmPt<T>temp(T&

);


——————-

这才是我们真正想要的。光只有这个还是觉得不够,我们应该可以取得他T*(既然这是智能指针,所以应该取得T的指针),所以我们可以添加一个函数,就叫他get()(share_ptr里面也有这么一个函数,同样也是获取T*),当然,这样做有一个缺点,安全性也就降低了,这就是一个安全性的漏洞,为什么呢?因为别人可能会通过这个函数获取到T*,然后对内部数据进行修改,这个问题就留给大家去解决吧。我们继续往下看。

到目前为此,这个智能指针也差不多了,我们来看看怎么实现,说到实现其实很简单,下面的实现如果有不明白的,大可以提出来:
——————————–
SmPt::SmPt():point(new T)
{ }

SmPt::SmPt(const T&

T0):point(new T(T0))
{ }

SmPt::SmPt(const SmPt&

T0):point(T0.point),
m_Count(T0.m_Count)
{ }

T* SmPt::get()
{

return point;


}
——————————-

现在我们还没有实现析构函数,为什么呢,因为这里遇到一个问题,因为我们是引用计数,所以我们无法知道什么时候该析构,为了能够得到这个反馈,我们可以在My_Count里面定义一个函数,让SmPt知道是不是该析构。
————————–
bool My_Count::onlyone()
{

if(*p==1)

return true;


return false;


}
—————————

有了这个反馈之后,我们就可以很简单的定义SmPt的析构函数了:
—————————-
SmPt::~SmPt()
{

if(m_Count.onlyone())

delete point;


}
—————————-

现在一切都好了,不过又发现,赋值操作符成了一个问题。因为这是引用计数,所以他看起来像My_Count里的,但又是他无法操作得了他的,不过我们可以给My_Count再添加一个辅助函数来实现,假如这个函数的名字叫mycopy():
——————————
bool My_Count::mycopy(const My_Count&

r)
{

++*r.p;



if(–*p==0){

delete p;


p == r.p;


return true;


}

p = r.p;


return false;


}
——————————

有了这个辅助函数,我们就可以轻松的完成SmPt的赋值操作符了。
—————————-
MsPt&

MsPt::operator=(const MsPt&

T0)
{

if(m_Count.mycopy(T0.m_Count))

delete point;



point = T0.point;


return *this;


}
—————————-

到这里,我们这个类就可以工作了,然后我们可以像下面这样使:
————————————-
SmPt<gameboard>My_Board(gameboard(10,10));


My_Board.get()->SetPiece(6,6,10);


cout<<My_Board.get()->GetPieceAt(6,6)<<endl;


SmPt<gameboard>My_Board2(My_Board);


My_Board2.get()->SetPiece(6,6,20);


cout<<My_Board2.get()->GetPieceAt(6,6)<<endl;


SmPt<gameboard>My_Board3;


My_Board3 = My_Board;


My_Board3.get()->SetPiece(6,6,25);


cout<<My_Board3.get()->GetPieceAt(6,6)<<endl;


—————————————

这个例子很明显,我们的gameboard里并没有赋值操作符和复制函数(虽然编译器为我们默认生成了,但是要是使用的话肯定会造成意想不到的错误),但是最好不要使,然而现在我们想要复制赋值等都很轻而易举,而且保证安全。

但是现在还有一个问题,我们使用了cout<<My_Board……,这是不允许的,为了允许,我们还得在SmPt里面重载operator<<。
—————————-
friend ostream&

operator<<(ostream&

out,const SmPt&

T0)
{

return out<<T0.point;


}
——————————

第五十四讲  引用计数(1)
当然很多时候可能我们还会像下面这样用:
——————————-
SmPt<SpreadSheetCell>n(SpreadSheetCell("

Hello"

));


cout<<"

n = "

<<*n<<endl;


SmPt<double>mydouble(6.6);


cout<<"

mydouble adress is:"

<<mydouble<<endl;


cout<<"

Mydouble is: "

<<*mydouble<<endl;


——————————–

大家看到了,n和*n是不一样的,因为我们这是智能指针,所以我们的SmPt的对象其实一个指针,想要得到这个地址里面的东西就得对指针解引用,要让上面的程序能够成功运行,我们还得添加一个解引用函数,实际上也就是重载operator*。
—————————-
T operator*()
{

return *point;


}
—————————–

第五十四讲  引用计数(1)
至此,我们这个SmPt差不多可行了,如果大家觉得还有什么补充的,可以自行补充,他可以用来替代一些对C++11支持不是太好的编译器缺少的share_ptr。

现在还有一个叫做写时复制的技术留待下一讲吧。

========================
回复D直接查看目录,有什么问题可以直接提问。


原文始发于微信公众号(

C/C++的编程教室

):第五十四讲 引用计数(1)

|

发表评论