以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」mp.weixin.qq.com/s/XIWsV7Lab...
首先,提个问题,何为引用计数,为什么要去实现引用计数?
智能指针最大的作用就是协助开发者管理内存,防止开发者遗漏释放或者错误释放已申请的内存空间,最大限度保证内存安全。使用智能指针可免除非常多的经典 C++ 版本带来的烦恼,比如招人恨的 double free or corruption。
据微软内部统计,C++ 应用的绝大部分问题属于内存安全问题,所以保障内存安全的措施真的非常重要。
那么,智能指针是如何保证内存安全的呢?
智能指针在初始化时会托管传入的内存空间,在后续的流程中,一旦识别到被托管的内存不再被需要的时候,自动释放这块内存。
如何识别被托管的内存是否还被需要?
通过引用计数,引用计数就是计数器,一般用整形变量表示,代表有多少个智能指针已托管该内存空间。
已托管的内存每增加绑定一个智能指针,该计数器自动加 1;反之,已托管的内存每解绑一个智能指针(比如智能指针超出生命周期,或者主动解除绑定),该计数器自动减 1。直到计数器归 0,这时被托管的内存即被判断为不再需要保留,最后解绑的智能指针负责释放该内存空间。
其中,可见该计数器被所有绑定的智能指针共享访问。
虽然标准库已经提供非常多现成的智能指针可供调用,但是其中的奥妙还是非常值得我们细细揣摩,下面就开始跟随笔者一起实现自己的引用计数帮助类,权当作略微简陋的自定义智能指针。
先定义两个类,一个是代表占用内存资源的类 Demo,另一个是就是我们自定义的智能指针类 SmartPtr
kotlin
class Demo
{};
class SmartPtr
{};
资源类附加属性
由于绑定同一个内存资源的所有智能指针是共享访问该内存的,所以可以在类 Demo 中附加定义计数器 count_,计数器作为专用属性应该带有隐私属性也就是不允许随意被类外访问,所以同时应该被修饰为 private。
在类 Demo 实例化时,计数器 count_ 应该被清零,然后才能被 SmartPtr 托管。所以要求类 Demo 的所有构造函数都初始化 count_ 为 0。
arduino
class Demo
{
public:
Demo() : count_(0) {};
private:
unsigned int count_;
};
作为托管方的类 SmartPtr 需要有权限可以直接读写类 Demo 的私有成员 count_,所以在类 Demo 中还要声明类 SmartPtr 为友元类
kotlin
class Demo
{
// ...
private:
// ...
friend class SmartPtr;
};
关于代表资源的类的简单改造就介绍完了。
自定义智能指针类
智能指针类 SmartPtr 作为托管方,需要实现基本的功能接口,包括托管、解绑、访问资源成员、获取资源引用等。
托管
智能指针需要托管某个内存资源,那么就需要在内部有对应的指针指向资源。同样为了不必要的暴露,这个指针需要用 private 修饰。
kotlin
class SmartPtr
{
private:
Demo *p_;
};
在托管资源时,资源对应的引用计数器应该自增 1,由于我们定义的智能指针在资源类的内部被声明为友元类,所以智能指针绑定资源时可以直接读写资源的计数器。
智能指针如何绑定资源,或者说如何接收将要被托管的资源呢?
资源未被托管
如果资源当前没有被任何智能指针托管,那么,可以在实例化智能指针对象时,利用智能指针的构造函数接收资源指针,并保存到内部指针变量,按照上面的代码就是保存到变量 p_。
javascript
class SmartPtr
{
// ...
public:
SmartPtr(Demo *p) : p_(p) {
++ p_->count_;
}
};
分享托管
如果资源当前正在被其它智能指针托管,那么,可以从其它的智能指针分享过来。
有两种分享的方法,一是创建一个新的智能指针来接收其它智能指针的分享,需要用到拷贝构造函数,并传入其它的智能指针的引用实例。
javascript
class SmartPtr
{
// ...
public:
SmartPtr(const SmartPtr &obj) : p_(obj.p_) {
++ p_->count_;
}
};
二是,如果有个智能指针实例当前已经托管某个资源,但是需要重新绑定其它资源,并且当前托管的资源需要解绑,可以利用拷贝赋值运算符,右侧操作数就是其它分享资源的智能指针,左侧操作数是当前的智能指针。
解绑当前托管的资源,除了要清除原有的资源指针备份,还需要对资源的计数器减 1,并且判断计数器减 1 后是否归零,如果归零就需要释放该资源占用的内存
ini
class SmartPtr
{
// ...
public:
SmartPtr& operator=(const SmartPtr& sp) {
Demo *p = p_;
p_ = sp.p_;
++ p_->count_;
if (0 == (-- p->count_)) {
delete p;
}
return *this;
}
};
生命周期的结束
上面提到更换托管资源时,还需要解绑自身原有托管的资源。其实当智能指针对象自身的生命周期结束之时,也就是调用释构函数时,也需要解绑自身原有托管的资源,这时的解绑主要做的就是对资源的计数器减 1,并且判断计数器减 1 后是否归零,如果归零就需要释放该资源占用的内存
arduino
class SmartPtr
{
// ...
public:
~SmartPtr() {
if (0 == (-- p_->count_)) {
delete p_;
}
}
};
成员访问
智能指针在托管资源后,资源内部的成员应该能通过智能指针来访问,正如前面介绍资源类时,在资源类内部声明智能指针为友元类。通过智能指针来访问资源类内部成员的形式,应该类似对象的指针访问对象成员,需要通过重写智能指针的成员访问运算符来实现
kotlin
class SmartPtr
{
// ...
public:
Demo* operator->() {
return p_;
}
};
operator-> ()
是一元右缀操作符,重写了类成员访问运算符,返回被托管的资源指针。
当智能指针对象调用该操作符时,比如 p 被声明为智能指针实例对象,被托管的资源所属类包含成员 m,那么 p->m
会被编译器解析成 ((p.operator->)->m
。
获取资源引用
一般通过对象指针获取对象引用时,是通过 *
运算符。类似地,需要获取被托管资源对象的引用,可以重写 *
运算符实现
kotlin
class SmartPtr
{
// ...
public:
Demo& operator*() {
return *p_;
}
};
避免暴露裸指针
一般不推荐直接调用资源类的裸指针,尽量避免重写智能指针的操作符以返回被托管资源的指针,因为暴露的裸指针会被意外地使用而破坏引用计数的机制,最终破坏实现内存安全的努力。
而上面在设计资源类时,仍然依赖使用 new 操作符创建资源实例并返回裸指针,这无疑是一颗定时炸弹。为了隐藏好裸指针,可以把构造函数声明为 private,并且添加静态成员接口 create() 返回智能指针对象
arduino
// .h
class SmartPtr;
class Demo
{
public:
static SmartPtr create();
private:
Demo() : count_(0) {};
unsigned int count_;
friend class SmartPtr;
};
// ...
// .cpp
SmartPtr Demo::create() {
return new Demo();
}
为什么接口 Demo::create()
内部直接 return 类 Demo 对象指针而不是 SmartPtr 对象?
因为前面实现类 SmartPtr 时,它的构造函数就有输入 Demo 指针的重载形式。所以接口 Demo::create()
声明返回类型为 SmartPtr 对象时,如果直接返回类 Demo 对象指针,就会隐式调用类 SmartPtr 的对应构造函数创建实例对象并返回。
基于上面的设计结果,当需要在堆里创建 Demo 实例时,内存安全的使用方式就可以是这样子
css
SmartPtr ptr(Demo::create());
简简单单的思路分析如上,如果各位同学朋友有什么疑问可以向我提,下拉到文章底部有我的联系方式。
最后,非常感激各位朋友的点 「赞」 和点击 「在看」,谢谢!