嗯,本来说到的周末不推送的,但碍于前两天加班没来及更大家更新,所以趁着今天嘴上长了个溃疡,疼得要命,也就没有出去玩,就给大家说一下新东西吧。
引用计数这个词大家想必都听说过,就好比说我们使用的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;
}
——————————
当然很多时候可能我们还会像下面这样用:
——————————-
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;
}
—————————–
至此,我们这个SmPt差不多可行了,如果大家觉得还有什么补充的,可以自行补充,他可以用来替代一些对C++11支持不是太好的编译器缺少的share_ptr。
现在还有一个叫做写时复制的技术留待下一讲吧。
========================
回复D直接查看目录,有什么问题可以直接提问。
原文始发于微信公众号(
C/C++的编程教室
):第五十四讲 引用计数(1)
|