STL源码剖析-智能指针shared_ptr源码
目录
一、 引言
二、 代码实现
2.1 模拟实现shared_ptr
2.2 测试用例
三、 潜在问题分析
你可能还需要了解模拟实现C++标准库中的auto_ptr
一、 引言
与auto_ptr大同小异,shared_ptr也是一个类。可以实现多个指针指向同一个对象(引用计数)。发生拷贝的话都指向相同的内存。
每使用一次,内部引用计数加1;
每析构一次,内部引用计数减1,;
引用计数减为0时,自动释放原生指针所指向的内存。
二、 代码实现
2.1 模拟实现shared_ptr
命名说明:为了和boost库提供的智能指针shared_ptr区分开,我将模拟实现的指针命名为mshared_ptr(m是my的简写)。
难点一、我们知道,boost库中提供的shared_ptr的核心就是引用计数,实现的方法不尽相同,只要能达到目的就可以了。在这里,我采用静态map表的方式来实现。
static map<T*, int> _map; //静态数据成员需要在类外进行初始化
如何理解这种操作?map表建立了原生指针T* 和次数一个映射。 如图1所示,如果有四个mshared_ptr(自主实现)类型的变量同时指向一块堆内存,map表中就会建立原生指针_ptr和4之间的一个映射。如果有更多的变量指向该块堆内存或者A、B、C、D其中有任何一个变量析构了,都会引起引用计数的变化。
图1 map表简要说明
难点二、 为什么成员运算符(俗称箭头)的重载返回类型是原生指针的类型?这一点在模拟实现C++标准库中的auto_ptr已经讨论过了。在这里再次讨论也无妨!mshared_ptr名为指针,实际上是类。对一个类采用成员运算符重载,返回值很自然的就是类中的成员了。
template<typename T>
T* mshared_ptr<T>::operator->() //成员运算符重载
{
return _ptr;
}
难点三、 引用计数是如何实现按需变化的?如下代码所示:if语句一定会进入,是否执行还得两说!if语句一经进入,引用计数就自减1了,在决定释放内存之前,万万牢记:不要对NULL指针进行操作,这就是if语句后半部分存在的意义。这小段代码在析构函数和赋值运算符重载中都出现了。值得注意一下。
if (--_map[_ptr] <= 0 && NULL != _ptr)
{
delete _ptr;
_ptr = NULL;
_map.erase(_ptr);
}
完整代码段:
#include<iostream>
using namespace std;
#include<map>
template<typename T>
class mshared_ptr
{
public:
mshared_ptr(T *ptr = NULL); //构造方法
~mshared_ptr(); //析构方法
mshared_ptr(mshared_ptr<T> &src); //拷贝构造
mshared_ptr& operator = (mshared_ptr<T> &src); //赋值运算符重载
T& operator*(); //解引用运算符重载
T* operator->(); //成员运算符重载
private:
T *_ptr;
static map<T*, int> _map; //静态数据成员需要在类外进行初始化
};
template<typename T>
map<T*, int> mshared_ptr<T>::_map;
template<typename T>
mshared_ptr<T>::mshared_ptr(T *ptr) //构造方法
{
cout << "mshared_ptr的构造方法正被调用!" << endl;
_ptr = ptr;
_map.insert(make_pair(_ptr, 1));
}
template<typename T>
mshared_ptr<T>::~mshared_ptr() //析构方法
{
cout << "mshared_ptr的析构方法正被调用!" << endl;
if (--_map[_ptr] <= 0 && NULL != _ptr)
{
delete _ptr;
_ptr = NULL;
_map.erase(_ptr);
}
}
template<typename T>
mshared_ptr<T>::mshared_ptr(mshared_ptr<T> &src) //拷贝构造
{
_ptr = src._ptr;
_map[_ptr]++;
}
template<typename T>
mshared_ptr<T>& mshared_ptr<T>::operator=(mshared_ptr<T> &src) //赋值运算符重载
{
if (_ptr == src._ptr)
{
return *this;
}
if (--_map[_ptr] <= 0 && NULL != _ptr)
{
delete _ptr;
_ptr = NULL;
_map.erase(_ptr);
}
_ptr = src._ptr;
_map[_ptr]++;
return *this;
}
template<typename T>
T& mshared_ptr<T>::operator*() //解引用运算符重载
{
return *_ptr;
}
template<typename T>
T* mshared_ptr<T>::operator->() //成员运算符重载
{
return _ptr;
}
2.2 测试用例
int main()
{
int *p = new int(10);
mshared_ptr<int>mshared_p1(p);
mshared_ptr<int>mshared_p2(new int(20));
cout << *mshared_p1 << endl;
cout << *mshared_p2 << endl;
system("pause");
return 0;
}
图2 VS2017运行结果
三、 潜在问题分析
在多线程环境下,引用计数可能会出错是不可避免的。但是通过加锁就能解决这个问题。本篇博客的关注点不在于多线程的环境下运行,故而未曾加锁。有一个问题,即使是boost库中的shared_ptr不可避免,那就是——循环引用(交叉引用)导致内存泄漏。现说明如下:
图3 循环引用示意图
mshared_ptr 利用引用计数来决定是否释放堆区的内存。如果存在循环引用的话,引用计数到最后还是会降不下去。如图3所示,类A只有成员_ptr_B,类B只有成员_ptr_A,如果发生上述情况,在ptr_A析构的时候,仅仅会将引用计数减1而不真正释放其所指向的内存;在ptr_B析构的时候也一样,究其根源,是因为类内的指针也占用了引用计数。
class B; //同文件,从上至下编译,故而需要告诉类A——类B确实存在
class A
{
public:
mshared_ptr<B>_ptr_B;
};
class B
{
public:
mshared_ptr<A>_ptr_A;
};
int main()
{
mshared_ptr<A>ptr_A(new A);
mshared_ptr<B>ptr_B(new B);
ptr_A->_ptr_B = ptr_B;
ptr_B->_ptr_A = ptr_A;
return 0;
}
图4 VS2017下验证示意图
从运行结果我们可以看到,ptr_A和ptr_B都已被析构,但是类内的指针没有被析构,这就是导致内存泄漏的罪魁祸首。如何解决这个问题,我们需要使用mshared_ptr的好搭档——mweak_ptr。模拟实现boost库中的weak_ptr 。
————————————————
版权声明:本文为CSDN博主「楚楚可薇」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41822235/article/details/82934681
STL源码剖析-智能指针shared_ptr源码的更多相关文章
- 【java集合框架源码剖析系列】java源码剖析之TreeSet
本博客将从源码的角度带领大家学习TreeSet相关的知识. 一TreeSet类的定义: public class TreeSet<E> extends AbstractSet<E&g ...
- 【java集合框架源码剖析系列】java源码剖析之HashSet
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于HashSet的知识. 一HashSet的定义: public class HashSet&l ...
- c/c++ 智能指针 shared_ptr 和 new结合使用
智能指针 shared_ptr 和 new结合使用 用make_shared函数初始化shared_ptr是最推荐的,但有的时候还是需要用new关键字来初始化shared_ptr. 一,先来个表格,唠 ...
- c/c++ 智能指针 shared_ptr 使用
智能指针 shared_ptr 使用 上一篇智能指针是啥玩意,介绍了什么是智能指针. 这一篇简单说说如何使用智能指针. 一,智能指针分3类:今天只唠唠shared_ptr shared_ptr uni ...
- C++智能指针shared_ptr
shared_ptr 这里有一个你在标准库中找不到的—引用数智能指针.大部分人都应当有过使用智能指针的经历,并且已经有很多关于引用数的文章.最重要的一个细节是引用数是如何被执行的—插入,意思是说你将引 ...
- 【java集合框架源码剖析系列】java源码剖析之LinkedList
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 在实际项目中LinkedList也是使用频率非常高的一种集合,本博客将从源码角度带领大家学习关于LinkedList的知识. ...
- 【java集合框架源码剖析系列】java源码剖析之TreeMap
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于TreeMap的知识. 一TreeMap的定义: public class TreeMap&l ...
- 【java集合框架源码剖析系列】java源码剖析之ArrayList
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 本博客将从源码角度带领大家学习关于ArrayList的知识. 一ArrayList类的定义: public class Arr ...
- 【java集合框架源码剖析系列】java源码剖析之HashMap
前言:之所以打算写java集合框架源码剖析系列博客是因为自己反思了一下阿里内推一面的失败(估计没过,因为写此博客已距阿里巴巴一面一个星期),当时面试完之后感觉自己回答的挺好的,而且据面试官最后说的这几 ...
随机推荐
- java番外茶余饭后闲聊
java番外茶余饭后闲聊 **本人博客网站 **IT小神 www.itxiaoshen.com 今天聊点题外话没事时可以作为平时沟通交流的谈资,接下来一起简单了解下个人知晓对Java界开发产生深远影响 ...
- Collection集合框架与Iterator迭代器
集合框架 集合Collection概述 集合是Java中提供的一种容器,可以用来存储多个数据 集合与数组的区别: 数组的长度固定,集合的长度可变 数组中存储的是同一类型的元素,可以存储基本数据类型值, ...
- 洛谷 P4569 - [BJWC2011]禁忌(AC 自动机+矩阵乘法)
题面传送门 又好久没做过 AC 自动机的题了,做道练练手罢( 首先考虑对于某个固定的字符串怎样求出它的伤害,我们考虑贪心,每碰到出现一个模式串就将其划分为一段,最终该字符串的代价就是划分的次数.具体来 ...
- AT695 マス目
AT695 マス目 本题选自 DP 优化方法大杂烩 状压部分. 这个题很 nb.下文记 \(n=H\),\(m=W\). 对于每一列,如果只记录一个格子是否为黑色,那么发现它无法处理从右边绕到左边再绕 ...
- 搜索工具Wox简单使用
目录 下载安装 几个常用命令 自定义 Wox是快速搜索小工具,内置了everything(需要先安装),但比everything好用.不止是搜文件,网页.系统等都可以快速搜索,还可以自定义. 下载安装 ...
- [R] cbind和filter函数的坑
最近我用cbind函数整合数据后,再用filter过滤数据,碰到了一个大坑. 以两组独立样本t检验筛选差异蛋白为例进行说明吧. pro2 <- df2[1:6] Pvalue<-c(rep ...
- iptables_超解
查询: -t选项,指定要操作的表,使用-L选项,查看-t选项对应的表的规则,-L选项的意思是,列出规则,所以,上述命令的含义为列出filter表的所有规则 显示出了3条链INPUT链.FORWARD链 ...
- Excel-vlookup(查找值,区域范围,列序号,0)如何固定住列序列号,这样即使区域范围变动也不受影响
突然,发现VLOOKUP的列序列号并不会随着区域范围的改变而自动调节改变,只是傻瓜的一个数,导致V错值.所有,就想实现随表格自动变化的列序号. 方法一:在列序号那里,用函数得出永远想要的那个列在区域范 ...
- 电脑盘符为什么从C盘开始?A盘和B盘去哪了?
虽然我们几乎每天都在跟电脑打交道,但是不知道大家有没有过疑惑,为什么电脑盘符是从C盘开始命名呢?有没有A盘和B盘??A盘和B盘去哪了??? 其实,A盘和B盘是真实存在的. 在早期的DOS时代,计算机的 ...
- A Child's History of England.3
So, Julius Caesar came sailing over to this Island of ours, with eighty vessels and twelve thousand ...