C++ | 再探智能指针(shared_ptr 与 weak_ptr)
上篇博客我们模拟实现了 auto_ptr 智能指针,可我们说 auto_ptr 是一种有缺陷的智能指针,并且在C++11中就已经被摈弃掉了。那么本章我们就来探索 boost库和C++11中的智能指针以及其实现方法。
文章目录:
一、独占型智能指针 scope_ptr
二、强 智能指针shared_ptr
三、弱 智能指针 weak_ptr
注:在本文中模拟的智能指针并不与库中的智能指针的实现完全相同,只是为了通过探究其实现原理而进行的一种模拟。
一、独占型智能指针 scope_ptr
在 boost中有一种 scope_ptr 指针,可以说这是boost库中最为简单的一种智能指针了。相对于前两种智能指针而言, scope_ptr 规定,一个智能指针只能引用一块堆内存,当这个指针的作用域消失之后自动释放。 scope_ptr 实现起来很简单,只需要将拷贝构造函数和赋值函数的接口屏蔽起来即可。
template<typename T>
class Scope_ptr
{
public:
Scope_ptr(T* ptr)
{
m_ptr = ptr;
}
~Scope_ptr()
{
delete m_ptr;
m_ptr = NULL;
}
T& operator*()
{
return *m_ptr;
}
T* operator->()
{
return m_ptr;
}
private:
Scope_ptr(const Scope_ptr<T>& rhs);
Scope_ptr<T>& operator=(const Scope_ptr<T>& rhs);
T* m_ptr;
};
缺点:同样的,没有什么是完美的,虽然在类中屏蔽了拷贝构造和赋值函数的接口,可是如果人为的去进行赋值,还是会出现多个智能指针指向同一片堆内存的情况。真是防不胜防啊。
int main()
{
int* p = new int;
Scope_ptr<int> sp1(p);
Scope_ptr<int> sp2(p);
Scope_ptr<int> sp3(p);
return 0;
}
我们可以看到,sp1、sp2、sp3,都指向内 p 所申请的堆内存中。而在对象销毁时,sp3 先进行销毁,同时会释放堆内存,而后的 sp2,sp1 就成为了悬挂指针,在进行销毁时就会出现内存重复释放的问题。
下面我们来介绍一种比较强的智能指针 shared_ptr 智能指针。
二、强智能指针shared_ptr
之前的几种智能指针方案都存在缺陷,它们在处理自身与引用的对象间的关系时总是不够理想。既然指针自己无法完美的管理与对象之间的关系,那么,我们就单独设计一个管理类用于管理指针与对象之间的引用关系。而这种设计理念也正是我们的 shared_ptr 类智能指针,我们一起来看看它又是怎么处理的呢。
首先说明 shared_ptr 这是一种强智能指针,强是相对于弱存在的,那么应该也存在一种弱智能指针喽?
是的,我们一会还要介绍一种弱智能指针 weak_ptr,这些放在下文在做讨论。抛开这些暂且不提,先讲 shared_ptr 实现,它的内部维护一个引用计数器来判断一块堆内存被引用的次数。
我们知道在字符串的写实拷贝实现中,通过设置一个引用计数区域来判断某片空间被引用的次数。同样的,我们在设计智能指针时也可以采取类似的方法。只不过我们的智能指针类可以有多个不同对象指向不同的堆内存。因此,在 shared_ptr 类中专门为其设置了一个引用计数管理器类。
clsaa Node
{
void* addr; /* 保存堆内存地址 */
int refCount; /* 保存该堆内存引用次数 */
};
通过在引用计数管理器类设计一种由结构体或者类封装的表,表中保存着申请的堆内存和其对应的引用次数。而在智能指针类中实现向表中添加数据,在某个堆内存的引用次数为0时,对该堆内存进行释放。大致模型如下:
下面是模拟实现 Shared_ptr 智能指针的具体实现
/* 引用计数管理类 */
class RefManage
{
public:
RefManage() : length(0) {}
/* 增加引用计数 */
void addRef(void* ptr)
{
if (ptr != NULL)
{
int index = Find(ptr);
if (index < 0)
{
arr[length].addr = ptr;
arr[length].refCount++;
length++;
}
else
{
arr[index].refCount++;
}
}
}
/* 删除一个引用计数 */
void delRef(void* ptr)
{
if (ptr != NULL)
{
int index = Find(ptr);
if (index < 0)
{
throw exception("addr is not exist");
}
else
{
if (arr[index].refCount != 0)
{
arr[index].refCount--;
}
}
}
}
/* 返回当前堆内存的引用计数 */
int getRef(void* ptr)
{
if (ptr == NULL)
{
return 0;
}
int index = Find(ptr);
if (index < 0)
{
return -1;
}
else
{
return arr[index].refCount;
}
}
private:
/* 查找是否是已经存在的堆区空间 */
int Find(void* ptr)
{
for (int i = 0; i < length; ++i)
{
if (arr[i].addr == ptr)
{
return i;
}
}
return -1;
}
/* 局部类,储存引用计数信息 */
class Node
{
public:
Node()
{
memset(this, 0, sizeof(Node));
}
public:
void* addr; /* 保存堆内存地址 */
int refCount; /* 保存该堆内存引用次数 */
};
Node arr[10]; /* 用数组模拟10个空间的引用计数器*/
int length; /* 有效结点个数、当前要插入的下标*/
};
/* Shared_ptr 智能指针类 */
template<typename T>
class Shared_ptr
{
public:
Shared_ptr(T* ptr = NULL) :m_ptr(ptr)
{
AddRef();
}
Shared_ptr(const Shared_ptr<T>& rhs)
:m_ptr(rhs.m_ptr)
{
AddRef();
}
Shared_ptr<T>& operator=(const Shared_ptr<T>& rhs)
{
if (this != &rhs)
{
/* 自身引用次数减一 */
DelRef();
/* 若引用次数为0,则立刻释放 */
if (GetRef() == 0)
{
delete m_ptr;
}
m_ptr = rhs.m_ptr;
AddRef();
}
return *this;
}
~Shared_ptr()
{
DelRef();
if (GetRef() == 0)
{
delete m_ptr;
}
m_ptr = NULL;
}
T& operator*() const
{
return *m_ptr;
}
T* operator->() const
{
return m_ptr;
}
private:
void AddRef()
{
rm.addRef(m_ptr);
}
void DelRef()
{
rm.delRef(m_ptr);
}
int GetRef()
{
return rm.getRef(m_ptr);
}
T* m_ptr;
static RefManage rm;
};
/* 静态成员变量在类外初始化 */
template<typename T>
RefManage Shared_ptr<T>::rm;
运行测试:
运行程序可以看到分别对 int 类型 、char 类型、double 类型初始化了三张表。我们在程序中动态申请了 int、char、double类型的堆内存空间。
执行到程序的最后一个语句时,我们可以看到在右侧对应的引用计数表中分别写入了每块堆内存地址和引用次数。
最后,在执行 return 语句时智能指针依次被销毁,并且在引用计数管理器中引用次数也在一并减少,直至某个引用次数为0时调用 delete m_ptr
析构掉堆内存。这一过程可通过调试观察到,有兴趣的同学可以自行调试观看。
单例模式的引用计数管理器
我们发现针对 int 类型、char 类型和double 类型的智能指针最终生成了三个不同的引用计数管理器,可是我们在设计引用计数管理器类的时候把保存内存地址的数据类型设置的是 void* 类型,就是为了能够保存不同类型的堆内存地址,如果像上图这样每定义一种智能指针就构造一个引用计数管理器未免太不高效了。
我们引用计数管理类针对不同的类型可以只生成一次,并且只需要生成一次。也就是说对于 shared_ptr 只需要一个实例的RefManage 就可以满足要求,这正好符合我们的设计模式中的单例模式。因此,我们可以将该智能指针的引用计数管理器类设计成为一个单例模式的类。点这里》》》快速传送门——单例模式的应用计数管理器
缺点,没错,还是有缺点
我们先来看一段代码:
class B;
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
Shared_ptr<B> pa;
};
class B
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
Shared_ptr<A> pb;
};
int main()
{
Shared_ptr<A> spa(new A());
Shared_ptr<B> spb(new B());
spa->pa = spb; /* 引用计数加一 */
spb->pb = spa; /* 引用计数加一 */
return 0;
}
在这段代码中就会发生内存泄漏,也就是本该析构的堆内存没有析构造成的。
如下图所示,分别为刚申请完 spa、spb时,执行完各自指针的互相指向时,已经调用完 return 后即将退出时:
我们在类A 和 类B 的析构中设计有打印函数,可以确定函数的调用情况,下面来看一下整个程序的输出情况
可以看到,在输出窗口只打印了构造函数而没有打印析构。也就是说从始至终都没有调用析构函数。
具体发生了什么让我们来看一张图就明白了。
简单来说就是,在两个堆内存对象中的智能指针互相指向对方,从而使得对方(这片堆内存被引用)的引用计数加一。但是栈上的指针指针只有两个。也就是说在程序结束时,系统自动清理栈上的两个智能指针,而两片空间的引用次数分别都为二。在栈清理完结束后,各自堆内存的引用计数只减少了一次,没有达到释放的条件,最终导致内存泄漏。
读者:(●´∀`●)你TM是不是在玩我?找茬?故意的吧,讲了这么多智能指针各个都有缺陷?再说正常写程序谁特莫互相引用着玩啊?
博主:别生气,别生气,听我慢慢给你解释。其实呢,对于我们日常中自己编写的应用,之前的那些智能指针都可以使用,甚至不用智能指针,只要你的程序逻辑够严密也不会发生内存泄漏。但是呢,作为一个库的提供者是面对所有编程人员的,每个人的编程习惯不同,使用的场景也不同,难免会产生纰漏。千里之堤毁于蚁穴,历史告诉我们千万不要不把编译器的警告不当回事,而且以上这种用法在某些场景确实是会用到的。好了,接下来我们进的 weak_ptr 就不会有什么问题了。
三、弱智能指针 weak_ptr
shared_ptr 是强智能指针 而 weak_ptr 是弱智能指针,那么这个‘强’ 与 ‘弱’ 又是如何定义和区分的呢?
我们可以这样简单的理解,强智能指针凡是引用就计数加一,而弱智能指针只引用不加一。并且weak_ptr的存在就是为了弥补 shared_ptr 的不足而诞生的。
weak_ptr 基于 shared_ptr ,它在引用堆内存时作为一个观察着的身份存在,在引用堆内存对象时仅仅获得资源的观测权。并且weak_ptr没有共享资源,不会引起指针引用计数的增加。
因此,对于上面的实例我们改成这样就不会出错了。
ps:我们自己写的Shared_ptr 是大写首字母,标椎库中提供的全是小写的类名,注意在这里我们使用标准库中的 shared_ptr 和 weak_ptr。
class B;
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
weak_ptr<B> pa;
};
class B
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
weak_ptr<A> pb;
};
int main()
{
shared_ptr<A> spa(new A());
shared_ptr<B> spb(new B());
spa->pa = spb; /* 引用计数加一 */
spb->pb = spa; /* 引用计数加一 */
return 0;
}
输出正常,也就是成功的释放的对象。
Weak_ptr 具体实现
Weak_ptr 的实现也非常简单,我们在之前设计的 Shared_ptr 中添加一个返回自身指针的接口给 Weak_ptr 使用。在 Weak_ptr 中的赋值函数中把强智能指针(Shared_ptr)的 m_ptr 赋值给弱智能指针Weak_ptr 的 m_ptr 即可。
template<typename T>
class Shared_ptr
{
public:
/*
省略部分代码……
*/
T* GetPtr() const
{
return m_ptr;
}
};
template<typename T>
class Weak_ptr
{
public:
Weak_ptr(T* ptr = NULL) :m_ptr(ptr) {}
Weak_ptr(const Weak_ptr<T>& rhs) :m_ptr(rhs.m_ptr) {}
Weak_ptr<T>& operator=(const Shared_ptr<T>& rhs)
{
/* 强智能指针给弱智能指针赋值 */
m_ptr = rhs.GetPtr();
return *this;
}
~Weak_ptr()
{
m_ptr = NULL;
}
T* operator->()
{
return m_ptr;
}
T& operator*()
{
return *m_ptr;
}
private:
T* m_ptr;
};
在我们使用的代码中如果有需要内部智能指针互相指向时,选择使用 Weak_ptr 弱智能指针即可。我们再次测试以上实例。
输出:A() B() ~A() ~B()
OK,完成。
至此我们已经成功模拟了 shared_ptr 和 weak_ptr ,并且经过简单的实验我们已掌握其用法。
最后,欢迎大家评论留言,相互学习。有错误的地方请大家帮忙指出,谢谢。
附:C++ | 智能指针初探 https://blog.csdn.net/weixin_43919932/article/details/104505178
C++ | 再探智能指针(shared_ptr 与 weak_ptr)的更多相关文章
- c/c++ 智能指针 shared_ptr 使用
智能指针 shared_ptr 使用 上一篇智能指针是啥玩意,介绍了什么是智能指针. 这一篇简单说说如何使用智能指针. 一,智能指针分3类:今天只唠唠shared_ptr shared_ptr uni ...
- C++智能指针shared_ptr
shared_ptr 这里有一个你在标准库中找不到的—引用数智能指针.大部分人都应当有过使用智能指针的经历,并且已经有很多关于引用数的文章.最重要的一个细节是引用数是如何被执行的—插入,意思是说你将引 ...
- STL源码剖析-智能指针shared_ptr源码
目录一. 引言二. 代码实现 2.1 模拟实现shared_ptr2.2 测试用例三. 潜在问题分析 你可能还需要了解模拟实现C++标准库中的auto_ptr一. 引言与auto_ptr大同小异,sh ...
- c/c++ 智能指针 shared_ptr 和 new结合使用
智能指针 shared_ptr 和 new结合使用 用make_shared函数初始化shared_ptr是最推荐的,但有的时候还是需要用new关键字来初始化shared_ptr. 一,先来个表格,唠 ...
- 智能指针shared_ptr新特性shared_from_this及weak_ptr
enable_shared_from_this是一个模板类,定义于头文件<memory>,其原型为: template< class T > class enable_shar ...
- 智能指针shared_ptr的用法
为了解决C++内存泄漏的问题,C++11引入了智能指针(Smart Pointer). 智能指针的原理是,接受一个申请好的内存地址,构造一个保存在栈上的智能指针对象,当程序退出栈的作用域范围后,由于栈 ...
- 智能指针 shared_ptr 解析
近期正在进行<Effective C++>的第二遍阅读,书里面多个条款涉及到了shared_ptr智能指针,介绍的太分散,学习起来麻烦.写篇blog整理一下. LinJM @HQU s ...
- C++11智能指针 share_ptr,unique_ptr,weak_ptr用法
0x01 智能指针简介 所谓智能指针(smart pointer)就是智能/自动化的管理指针所指向的动态资源的释放.它是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保自动正确的销毁动 ...
- C++11--智能指针shared_ptr,weak_ptr,unique_ptr <memory>
共享指针 shared_ptr /*********** Shared_ptr ***********/ // 为什么要使用智能指针,直接使用裸指针经常会出现以下情况 // 1. 当指针的生命长于所指 ...
随机推荐
- 聊聊你对AQS的理解
场景引入 面试官上来就一句,谈谈你对AQS的理解,大家心里可能收到了1W点伤害,AQS是什么,可能连全称都不知道,所以下面让我们聊聊AQS. 以ReentrantLock来介绍一下AQS 在java中 ...
- shell脚本创建身份证号
--作者:飞翔的小胖猪 --创建时间:2021年5月16日 --修改时间:2021年5月16日 说明 运行脚本,用户手动输入信息生成身份证号.该程序的核心在于函数模块化及select的使用. 注意:该 ...
- 流程控制、if、elif、else,whilie、break、continue的使用
今日内容 流程控制理论 if判断 while循环 流程控制概念 流程控制就是控制事物的执行流程 执行流程的分类 顺序结构 从上往下依次执行,代码运行流程图如下 分支结构 根据某些条件判断做出不同的运行 ...
- Linux——vi命令详解
转载 Linux--vi命令详解 原文链接:https://blog.csdn.net/cyl101816/article/details/82026678 vi编辑器是所有Unix及Linux系 ...
- CF1548B题解
在日报上面看到的,发现 NOIP 模拟赛考过这个 trick( 首先我们把题目要求的条件这么写: \[a_i=x_i \times m+k \] 那么我们要找到满足条件的数组,差分后的数组一定都是 \ ...
- vue项目部署到阿里云服务器(windows),Nginx代理!
项目构成: 前端:vue+vant-ui, 数据库:mysql, 后端:node.js 部署方式:nginx代理: 一,首先要拥有自己的服务器,阿里,腾讯都可以,我用的是阿里的: 如果只是做个人项目的 ...
- 如何写好B端产品的技术方案?
B端产品为企业提供协同办公的工具,帮助企业解决某类经营管理问题,核心价值在于为企业增加收入.降本提效.管控风险,企业级SaaS产品也是B端产品中的一类. B端产品有以下特点: 客户是一个群体:B端产 ...
- 【公告】淘宝 npm 域名即将切换 && npmmirror 重构升级
镜像下载.域名解析.时间同步请点击阿里云开源镜像站 前言 本文将包括两部分内容: 淘宝 npm 域名即将停止解析 npmmirror 镜像站大重构升级 原淘宝 npm 域名即将停止解析 正如在< ...
- java中自己常用到的工具类-压缩解压zip文件
package com.ricoh.rapp.ezcx.admintoolweb.util; import java.io.File; import java.io.FileInputStream; ...
- 反射操作dll类库之普通类和各种方法调用
一.使用方法 查找DLL文件, 通过Reflection反射类库里的各种方法来操作dll文件 二.步骤 加载DLL文件 Assembly assembly1 = Assembly.Load(" ...