C++ 智能指针
我们之前经常提到内存泄漏问题,内存泄漏就是指程序没有释放掉不再使用的内存。内存泄漏不是指物理意义上的内存消失,⽽是应⽤程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。内存泄漏的危害,其实对于普通程序运行的话,即使内存泄漏了也问题不大,进程正常结束的时候父进程会来处理,释放内存。但是,长期运行的程序,比如说操作系统,后台服务,长时间运行客户端等等,出现内存泄漏会导致可
文章目录
C++ 智能指针
1. 智能指针的使用场景
C++ 为什么要引入这个智能指针呢?主要还是因为C++在这个内存管理方面,有些处理起来比较麻烦的点。C++ 不像java 等语言那样,有这个 GC 也就是垃圾回收器,C++里面处理内存泄漏是要我们自己处理的。
但是有的时候却是会因为一些逻辑上的地方,比如说抛异常了,导致我们的程序明明 delete 但是抛异常跳出去了,导致内存泄漏了。我们要避免内存泄漏嘛。智能指针就是来解决内存泄漏问题的。
C++ 为什么不要垃圾回收器呢?主要还是因为哭也算时间,开个玩笑。主要是因为这个垃圾回收器也是要消耗调一部分性能的,C++ 呢主打就是高效,加上垃圾回收器虽然方便但是会失去他自己的一些的特点。
2. 智能指针的引入
C++ 里面怎么做的呢?首先是提出了一个 RAII 的思想, 即: 资源获取立即初始化, 翻译成人话就是把这个要释放的资源交给一个对象来管理,这个在这个对象生命周期结束的时候,无论如何都会调他的析构,来把这个他管理的资源给释放掉。即使是抛异常,我们说他栈展开,一层一层往回抛,出某个函数栈帧的时候,也会自己调用这个栈帧里面对象的析构,也不会发生内存泄漏问题。
其实 Java 等语言也会有用到智能指针的地方。 java 都有垃圾回收器了,为什么还要这个智能指针?这里涉及到线程方面的知识,浅浅一谈,后续linux的时候再补充。线程里面的线程安全问题的板块有一个概念叫加锁,加锁和开锁之间的区域叫临界区。
如果有两个线程同时对一个变量或者什么做一些什么的话,比如说给 i++ ,这个 i 其实不一定是加到 2 ,也有可能是 1 ,比如两个线程同时拿到这个 i 的初值的时候都是 0, ++以后都变成 1, 最后放回来还是 1 。为什么会这样呢,因为这个 i 不是原子的,这个我们也后面再说。
可以看到,两个线程同时处理这个 i 的话是会导致结果异常的, i 是线程不安全的。我们期望的是一个线程走完了以后,另一个现在再走,这个时候,我们就要给这个 i 加锁,让同一时间只能有一个线程来访问 , 换句话说,就是要把这个 i 放到加锁和开锁的临界区里面。
但是,如果这个解锁之前万一有点抛异常什么的,导致我们上锁以后没有解锁,这个东西我们叫他死锁,这是不好的。
所以,我们就还是用到了 RAII 的思想,把这个上锁和开锁交给一个对象的,这个对象生命周期结束了以后,自己调析构来解锁。 其实不止 java , python 等等语言也会用到这个类似的RAII的思想,就比如处理这里的线程加锁的问题。
其实这个 RAII 的思想呢, 在 C++ 98 之前就提出来过,但是一直没有完善,主要是因为这个对象的拷贝问题。
3. C++ 标准库智能指针的使用
下面我们再来看看这个智能指针的拷贝问题,首先,拷贝肯定不能深拷贝,这里和容器哪里是不一样的。容器那里的拷贝,主要是我拷贝一个容器,我是想让我这个新的容器完全拥有有这个拷贝容器的资源,所以,我才需要深拷贝开空间什么的。
但是智能指针不同,我不是想让他变成这个资源,我是想让他代管这个资源:

所以,我们这里肯定不能深拷贝,我们应该是要浅拷贝。但是浅拷贝也有问题,就是析构多次,不处理的话依然会报大错。
当然,智能指针作为一个“指针”,还是需要支持 -> ,[] 等操作符的,操作符重载就好
那么我们应该怎么处理呢?
首先,我们先来认识一下智能指针,C++ 标准库的智能指针都在 头文件里面。这里面有好几个智能指针,除了 weak_ptr ,别的都是 可以RAII思想来代管资源的,原理上还是拷贝的不同。
auto_ptr 这是 C++ 98 实现的智能指针,这个设计非常糟糕,他是一种管理权的转移。什么意思,他在拷贝时会把被拷贝对象置空,相当于是把管理权完全给给拷贝对象。这个一看就是个大坑啊,拷贝一下直接把原指针置空会直接导致我们很容易访问到空指针,我们也是不希望这样的,所以,在 C++ 11 新的智能指针设计出来以后,这个就被强烈建议不再使用了。
其实C++ 11出来之前,很多公司也是命令禁止使用这个智能指针的。
unique_ptr, unique_ptr 和这个 auto_ptr 差不多。他的底层实现是里面左值的构造和拷贝构造封掉了,禁止使用了,右值的构造和这个移动构造倒是可以的,用了一个万能模板参数接受。等于说,如果要用 unique_ptr 更安全了一些,因为在我们真正在操作的时候,是要给他传右值的,右值置空就没什么大问题。等于说不需要拷贝的地方有他还是不错的。
C++ 11 里面还引入了share_ptr 和这个 weak_ptr ,这四个智能指针里面 shared_ptr 是最重要的。在谈后面两个智能指针的时候,我们先来聊聊C++ 的历史。C++ 经常被骂主要一个原因还是更新太慢,可以看到这个 C++ 98 和这个 C++ 11之间隔了 13 年,其实是非常长的啊。
差不多 九几年,零几年的时候,C++ 是遥遥领先的,别的语言还都没有出生,C++ 一家独大,估计也是想着这个短时间没有人能超过他,还是比较懒的。那时候主要是PC互联网时代,personal computer ,C++ 主要还是用来结合这个 mfc 来做一些界面开发,或者桌面软件的开发,mfc只windows的一个库。 当时C++ 感觉自己没有人能超过,没有意识到危机,当时定的是五年一更, 结果到了 03 年的时候,其实就是只修复了一些 98 的一些不好的地方,也没有什么新特性。
所以,如果看见什么C++ 98 -03之类的,其实都差不了多少。
后来本来说是 07 08年的更新一波来着,结果一直憋到了 11 年,就是想来波大的。C++ 11 呢说实话新特性更好肯定是更好了,但是相对的学习成本也越来越高了。11 年附近其实也是一个变革点,从PC到了移动互联网时代,主要因为智能手机的产生,越来越多的人开始上网,同时也更加方便了,比如说打个车之类的。因为用户越来越多呢,app 和 这个应用开发发展的就很不错,也产生很多的新语言。后续C++ 的更新也渐渐的就是正常起来。
23 年附近,也是到了 AI + 万物互联的时代,ai 早些年间就提出过,当时算法一类还不是很成熟,后来chatCPT产生,也标志ai发展也开始加速,算是转折点吧。万物互联的话当时就听说什么衣服裤子也能联网之类的,哈哈。
这一阵子 rust 发展势头其实也很猛的,这个语言和C++都是主打效率的,rust有点在于它的内存管理部分做的挺不错的。
但是这个 C++ 也在憋大的,比如说 C++ 26 新引入了什么反射的自检查机制之类的。
这里插播一句啊,C++ 真要全记住地包圆几乎是不可能地,知识量还是很大的,不要在简历里面写精通C++奥,要不然估计死的很惨。我记得前一阵子看过《effective C++》,Scott Meyers 在C++ 17 还是什么的时候就意识到 C++ 的知识已经庞大到一个人是不能完全掌握的地步了,后来写了一本封笔做 morden C++ 之后就退休过日子取了,hah。
C++ 更新呢还有一个比较重要的库叫 boost , 这个呢是C++ 的一个准标准库,组织者也是这个 C++ 标准委员会的一个大佬,那这个库什么意思呢,主要还是嫌C++ 更新太慢了,C++ 标准委员会经常因为一个新的点吵好长时间, boost 就没有这个顾虑了,它自己就去实现一些新的东西,C++ 标准委员会呢要是觉得好,就抄抄。
C++ 里面不少东西都是从这个 boost 里面抄来的,比如说这个右值引用,还有这个智能指针。但是,也不是全抄来着,boost 里面有个侵入式的智能指针不太合适就没抄,再比如这个 unique_ptr 就是借鉴的这个 boost 里面的 scoped_ptr ,boost 里面也有这个 shared_ptr ,C++ 也是借鉴的这个,当然也不完全一样。
其实这也是C++委员会被骂的原因啊,讨论那么长时间最后也还要靠抄的。
4. 智能指针的原理
4.1 原理和简单模拟实现
之前我们讲到过的 auto_ptr 和这个 unique_ptr 的原理其实就是管理权转移,只不过这个 unique_ptr 是封掉了左值的拷贝构造和赋值拷贝,但是啊右值的可以,通过一个万能引用,等于说就是让程序员可以有意识地去使用右值。
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为⾃⼰给⾃⼰赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针⼀样使⽤
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
这就是 auto_ptr 的一个大概的底层实现。
template<class T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针⼀样使⽤
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 重点在这里,可以看到这里的拷贝构造和赋值拷贝都封掉了
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
// 但是是支持右值的移动构造和移动赋值的, 可以看到思想还是一个管理权转移
unique_ptr(unique_ptr<T>&& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T>&& sp)
{
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
}
private:
T* _ptr;
};
其实这里最重要的不是上面两个智能指针,而是这个 shared_ptr 。
shared_ptr 在管理权的问题上就不是再只用的管理权转移,而是使用的是这个多个shared_ptr可以管理同一个资源的。
那么这个要怎么做到的呢?首先或许大家会想到给这个shared_ptr加一个成员变量,这个 count 的成员变量的值就是一共有几个智能指针管理着的。 行吗?肯定不行。 思考一下,这样处理的话,每一个shared_ptr都有自己的一个计数,这是不合理的,我们想要的是让这个同一个资源的shared_ptr 共用一个计数。
如果把这个成员变量整成静态的呢? 也是不行的。 我们是想要 管理同一个资源的 shared_ptr 共用一个计数,变成静态的成员变量那所有的 shared_ptr ,不管是不是管理同一个资源的,这个 shared_ptr 类整个共用一个计数了,这肯定也是不行的。
那怎么办呢?就是使用 引用计数, 一个资源一个引用计数。如何实现这个引用计数呢? 也很简单,只要给 一个资源搭配开辟一块空间存这个引用计数,然后这个 shared_ptr 里面放的是指向这个有引用计数的指针就好了。


这样就处理好咯,每一个资源一个搭配一个引用计数,来表示这个 shared_ptr 指向这个资源的有几个。
所以,要实现一个简易版本的 shared_ptr 主要就是在这个引用计数的处理上:
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
void release()
{
if (--(*_pcount) == 0)
{
// 最后⼀个管理的对象,释放资源
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
_del = sp._del;
}
return *this;
}
~shared_ptr()
{
release();
}
//.....
private:
T* _ptr;
int* _pcount;
};
可以看到。这里的 shared_ptr 的构造函数,在构造的时候同时给这个资源开辟一个空间, 因为什么时候资源来了,构造的时候资源来了。
在拷贝构造的时候,直接用这个被拷贝的 shared_ptr 的内容来拷贝,构造就好。
然后这个 赋值重载的时候, 要考虑自己给自己赋值的情况,这里我们用这个管理的资源来判断是否是同一个 shared_ptr , 然后不同的 shared_ptr 赋值的话现考虑是不是最后一个,如果是最后一个就直接释放掉资源。 这就是一个简单版本的 shared_ptr 的一个实现,可以注意到这个 release 里面用到了一个 _del 这个是一个定制删除器的概念,后续我们会详细说。
库里面对这个 shared_ptr 的实现还有好多要考量的地方,我们这里只是简版中的简版。
细心小伙伴可以看到这里的智能指针的构造加上了explict ,表示不可以走我们之前提到的隐式类型转换去初始化了。为什么呢,主要是为了防止普通指针隐式类型转换成智能指针。
4.2 定制删除器
不知道大家有没有发现华点,智能指针我们想让他来管理资源,就是走个构造,让他指向要管理资源的地址,这好实现,但是,这些资源是不一样的呀?什么意思,比如说有的是 new 出来的, 有的是 new[] 出来的。甚至有的 是fopen打开的,当我们要释放这些资源的时候,智能指针的析构怎么写?
所以,我们就引入了这个定制删除器的概念,看名字就知道是要给这些资源定制一个删除器,换句话说就是一个可调用对象,谁来定制,那程序员定制肯定,定制完了以后交给这个智能指针,然后智能指针要析构或者说要释放资源了,就调用我们传进去的这个定制删除器的对象就好了。
我们先来看share_ptr是怎么处理定制删除器的,

其实大家学到现在了也差不多能感觉出来,定制删除器肯定要用模板,shared_ptr 这里的处理是把构造函数写成了一个函数模板,这样我们在使用这个 shared_ptr 的时候在传入被管理对象的时候,顺带传上这个定制删除器就好了,
我们还是以一个日期类为例:
shared_ptr<Date> sp1 (new Date); // 这是不穿定制删除器的方法
shared_ptr<Date> sp2 (new Date, Destruct_Date);
shared_ptr<Date,Destruct_Date> sp3 (new Date, Destruct_Date);
可以看到后面三个都是这个传入定制删除器的写法,要注意,这里的删除器一定要是一个可调用对象,例如:函数指针,仿函数,或者 lamada 对象。 从这个地方我们也看出来,这里的用法是非常方便的,可以不显示实例化,直接传参让编译器推导,也可以显示实例化,然后传参。
对了,这里的可调用对象如果是个函数模板传参的话,记得显示实例化一下, 这样才算类型
还有一个特殊注意的点是什么呢?如果是 new[] 出来的话,显示实例化的时候可以把 [] 传进去,就可以不用定制删除器了,可以理解为就是模板的特化,单独给这个 [] 的情况特化了一份出来,还是以这个 Date为例:
shared_ptr<Date[]> sp1 (new Date[5]);
shared_ptr<Date> sp2 (new Date[5], DeleteArrayFunc<Date>) //也可以定制删除器
这里的 shared_ptr 的定制删除器看起来挺方便的,但是这个 unique_ptr 的定制删除器的用法和这个 shared_ptr 不不一样。主要是模板写的位置不一样,这个shared_ptr 是把构造函数写成了一个函数模板,unique_ptr 呢是在这个类模板的模板参数里面加上了这个定制删除器:

那这样其实是不如 shared_ptr 方便的。
这个在使用的时候就必须要求显示实例化这个unique_ptr ,这样的化使用这个 lamada 表达式就很不方便。我们之前讲过这个 lamada 的实现是仿函数,类名是 lamada + UUID,这个类型名我们是不能直接拿来用的,但是可以用模板或者auto来让编译器自己推导,我们拿对象去使用,但是显示实例化就要求我们要写个类型名进去,咋办?
其实也又办法,就是 decltype 这个函数,我们传入一个对象,他返回一个这个对象的类型名:
所以我们就可以这样:
auto fcloseFunc = [](FILE* ptr) {fclose(ptr); };
std::unique_ptr<FILE, decltype(fcloseFunc)> up4(fopen("Test.cpp", "r"), fcloseFunc);
还要注意的一个点是这个 decltype 返回的这个类型不是个字符串,额,后期更详细地我们再说。
这里用 lamada 还有不方便的地方,因为 lamada 表达式的对象是不能默认使用构造的,因为这个类里面把这个构造给封掉了,所以,这里还不能只显示实例化进去让他自己构造一个,只有类型生成对象生成不出来,还必须就是再传一个对象进去拷贝构造一下。如果是函数指针的话,这里也要显示传一下。
所以这里的 unique_ptr 使用的时候推荐使用防函数。
这里综合看下来还是这个 shared_ptr 在这个定制删除器上搞得还是好一些。那么他是怎么实现的呢?我们来修改一下我们之前的代码:
template<class T>
class shared_ptr
{
public:
...//
template<class D>
shared_ptr(T* ptr , D del)
:_ptr (ptr)
,_pocunt(new int(1))
, ?
}
我们尝试实现一个带函数模板的定制删除器的时候,发现构造的时候要走初始化列表,但是这个类里面没有这个删除器类型的成员变量,咋办?一个办法就是像 unique_ptr 那样,直接把删除器变成模板参数。这个我们之前也提到过,不好用,哈哈。
第二个就是什么呢?包装器。
之前我们说过包装器可以包装各种可调用对象,那么这里的话就包装一个返回 void 的以 (T*) 为参数的可调用对象不就好了嘛:
.../
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del;
这样走一个包装器,这个构造的时候,成员变量的问题就解决了。但是还有一件事,我们正常的不传这个删除器的时候,我们希望他走的是这个 delete ,但是这样好像我们没次都要自己传一下了,而且function 默认构造不可调用,它内部会自己检查,查到了会崩。那咋办?
给这个包装器一个缺省值不就好啦,走初始化列表的时候,不写直接走这个缺省值,这里我们用一个 lamada 就好:
function<void(T*)> _del = [](T* ptr) { delete ptr; };
这样就可以了,完美解决。其实当时没有包装器的时候,是用另外的类来实现的,这个我们就不细讲了。
4.3 make_shared 的使用
除了我们之前提到的直接构造这个 shared_ptr 对象, 还有一种办法就是 用这个 make_shared 函数来构造一个 shared_ptr , 作用类似我能之前提到过的 make_pair

用法上这个:
shared_ptr<Date> sp1 = (new Date(2026,4,30));
shared_ptr<Date> sp2 = make_shared<Date>(2026,4,30);
这样的两个用法是一样的,几乎没有区别。那为什么还有 make_shared 的这个东西呢?
首先,在使用上是没有区别的,但是在底层是有区别的。shared_ptr 不是有引用计数嘛,我们之前是把这个引用计数 构造的时候 new 出来,这个顶多也就是一个整形变量 4 字节或者更大,那这样就会造成一些内存碎片问题。而且这样大量的申请小块内存也是对堆是有消耗的。
如果我们直接构造这个 shared_ptr 的话,可以发现,我们是现创建对象,然后再开辟空间,这两个东西的内存是不连续的
,所以会出现内存碎片的问题。如果我们使用 make_shared 的话,我们使用的时候是没有先创建对象,而是直接传参给这个函数,所以,其实,这个函数内部会帮我们开辟空间,构造对象,这个开辟空间的时候,顺带就把这个引用计数和这个对象的空间开辟到一起,在最开始开辟一个整形的大小存引用计数。 类似于这个 new[] ,会在最开始存个数一样。
这样就解决了这个内存碎片问题,而且开空间还只开一次,还是比较不错的。
但是不管怎么说, shared_ptr 使用起来都是要维护这个引用计数的,要维护就要有消耗和成本,所以,如果不需要拷贝的情况下,还是 unique_ptr 更香一些。
最后一点需要注意的是,shared_ptr 和 uniuqe_ptr 都支持 operator bool 的隐式类型转换,等于说我们可以直接使用这个智能指针当成判断条件,来看看他有没有管理资源,false就是没有,true就是有。 也算是一种从自定义类型向内置类型的转换。
5. weak_ptr
5.1 循环引用
之前我们在说智能指针的时候,提到过 weak_ptr ,严格意义上来说它不能叫智能指针,因为他不管理资源,也不增加引用计数。这个 weak_ptr 设计出来是为了什么呢?是用来解决 shared_ptr 的缺陷的,即: 循环引用。
什么叫循环引用呢?我们来看下面这个链表的例子:
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2;
n2->_prev = n1;
return 0;
}
结构体类定义的是双链表的节点,只不过是把普通的指针换成智能指针了,然后主函数里面 new 出来两个节点,把他们链接起来,看起来很正常是吧?其实,就这样操作的话就已经内存泄漏了,这个两个 listnode 都不会走析构。
为什么?来看下面这个图:

new 出 n1 和 n2 的时候,是这样的,没什么问题,然后把他们两个双向链接起来:

这样,前后链接起来,然后多了两个 share_ptr 指向,然后引用计数变成 2。
然后 n1 和 n2 析构,引用计数分别减一,所以,两个节点的引用计数都是 1 。因为有 节点里面的这个 next 和 prev 架着,所以,内存没有释放。
那右边的节点什么时候释放呢?右边的节点被左边的 next 管着, next 析构了,左边节点释放,右边节点就释放了。
那左边的的节点什么时候释放呢?左边的节点被右边的 prev 管着,prev 析构了,右边节点释放,左边节点也就释放了。
看出问题来了吗?这个两个哥们在这相互拉扯转圈,到最后谁也释放不了,这就造成内存泄漏了。
为了解决这种循环引用的场景,我们设计了 weak_ptr ,来处理这种循环引用的情况。(说实话循环引用场景不是很多)
5.2 weak_ptr
首先,weak_ptr 不支持 RAII, 也不支持访问资源,所以我们看文档的时候:

这是 weak_ptr 的构造函数,可以看到这里的 weak_ptr 是不支持绑定资源,只支持绑定到 shared_ptr 的,支持 shared_ptr 的构造,赋值,不增加 shared_ptr 的引用计数。
想一下不增加引用计数的话,那么我们在 设计 next 和 prev 的时候,把这个两个指针换成 weak_ptr 这样就可以解决引用计数多加一次,而循环引用的问题了。
struct ListNode
{
int _data;
// 这⾥改成weak_ptr,当n1->_next = n2;绑定shared_ptr时
// 不增加n2的引⽤计数,不参与资源释放的管理,就不会形成循环引⽤了
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
我们还需要注意的是,weak_ptr 因为不参与资源管理,他不重载 operator* 和 operator-> , 而且他的你热播构造也是无参的,因为他就相当于是一个指针
换句话说,shared_ptr 害怕循环引用, weak_ptr 害怕什么? 害怕他自己悬空,也就是他绑定的 shared_ptr 已经释放了,weak_ptr 还在指着他。相当于大哥逃跑了,weak_ptr 就是跟着大哥的小弟。
所以啊, weak_ptr 不增加引用计数,但是不代表他没有引用计数, weak_ptr 的一个成员函数叫 expired ,就是来判断这个 weak_ptr 有没有过期的。

但是这样又引出一个问题,这个引用计数什么时候释放呢?我要怎么知道有多少个 weak_ptr 看着这个引用计数,直接让 shared_ptr 析构的时候带走肯定是不行的。
很简单,再来一共引用计数来管理这个引用计数就好了,这个新引用计数就标记着有多少个 weak_ptr 和这个 shared_ptr 绑定,所以,仿照 shared_ptr 的设计思路,再来一个类就好了。
所以,我们上面实现的是简单到不能再简单的 shared_ptr , boost 的源码里面shared_ptr 设计了上前行差不多,就是为了来解决这个循环引用,适配这个 weak_ptr 等等。上面提到的引用计数的引用计数在这个 shared_ptr 里面也有体现。
除了上述我们提到过的expired 成员函数, weak_ptr 里面还有这个 use_count 函数来看看引用计数,这个 shared_ptr 里面也有,源码里面有个 pn 就是当前的引用计数。
weak_ptr 还有一个 lock 的成员函数,这个的意思就是锁住资源,翻译成人话就是,如果 weak_ptr 要访问资源时,可以先 lock 返回一个管理资源的 shared_ptr 对象, 如果资源已经被释放,返回的 shared_ptr 时一个空对象,如果资源没有释放,就可以通过返回的 shared_ptr 访问资源,这是安全的。
底层其实就是再新产生一个 shared_ptr, 之前不是说 weak_ptr 是小弟来着,这就相当于自己给自己产生一个大哥,如果原来和他 绑定的 shared_ptr 置空了,他依然是可用的。
相当于是自己显示增加一个引用计数。
*6. shared_ptr 的线程安全问题
线程部分在这里的话还没有学到,所以这里就简单听一下。
shared_ptr的引⽤计数对象在堆上,如果多个shared_ptr对象在多个线程中,进⾏shared_ptr的拷
⻉析构时会访问修改引⽤计数,就会存在线程安全问题,所以shared_ptr引⽤计数是需要加锁或者
原⼦操作保证线程安全的。
shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr
管,它也管不了,应该有外层使⽤shared_ptr的⼈进⾏线程安全的控制。
所以,我们在处理这样的情况的时候,第一种办法就是加锁,第二种就是要进行原子操作

这个 atomic 翻译过来是原子的,把这个 int 给给这个 atomic 这样他就是线程安全的了,头文件是
总结一句话,库里面 shared_ptr 是设计好了的,引用计数是线程安全的,换句话说 shared_ptr 本身是线程安全的,但是他指向的资源不是线程安全的,他想管也管不住。 所以,对于资源这部分就会有线程安全问题。
*7. 内存泄漏
7.1 什么是内存泄漏,内存泄漏的危害
我们之前经常提到内存泄漏问题,内存泄漏就是指程序没有释放掉不再使用的内存。内存泄漏不是指物理意义上的内存消失,⽽是应⽤程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
内存泄漏的危害,其实对于普通程序运行的话,即使内存泄漏了也问题不大,进程正常结束的时候父进程会来处理,释放内存。但是,长期运行的程序,比如说操作系统,后台服务,长时间运行客户端等等,出现内存泄漏会导致可用的内存不断减少,最后会卡死。
7.2 如果检测内存泄漏
linux 下的工具:
[]: https://blog.csdn.net/gatieme/article/details/51959654
windows 下的第三方工具 https://blog.csdn.net/lonely1047/article/details/120038929
但是听说不靠谱,哈哈。
7.3 如何避免内存泄漏
主要分为两个办法,第一个就是事先预防,第二个就是事后差错,这边主要推荐第一种。
养成良好的编码规范,申请的内存空间记得释放,有抛异常这种来回带哦的情况,用好智能指针。
尽量采用智能指针来管理资源,如果场景投特殊,也可以RAII思想自己造个轮子。
最后就是定期使用内存泄漏工具检测了,有的要钱,有的不靠谱。
的内存。内存泄漏不是指物理意义上的内存消失,⽽是应⽤程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
内存泄漏的危害,其实对于普通程序运行的话,即使内存泄漏了也问题不大,进程正常结束的时候父进程会来处理,释放内存。但是,长期运行的程序,比如说操作系统,后台服务,长时间运行客户端等等,出现内存泄漏会导致可用的内存不断减少,最后会卡死。
7.2 如果检测内存泄漏
linux 下的工具:
[]: https://blog.csdn.net/gatieme/article/details/51959654
windows 下的第三方工具 https://blog.csdn.net/lonely1047/article/details/120038929
但是听说不靠谱,哈哈。
7.3 如何避免内存泄漏
主要分为两个办法,第一个就是事先预防,第二个就是事后差错,这边主要推荐第一种。
养成良好的编码规范,申请的内存空间记得释放,有抛异常这种来回带哦的情况,用好智能指针。
尽量采用智能指针来管理资源,如果场景投特殊,也可以RAII思想自己造个轮子。
最后就是定期使用内存泄漏工具检测了,有的要钱,有的不靠谱。
总之,内存泄漏还是挺常见的,首推还是事先预防,一些单位也是期望或者说要求程序员实现预防的。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)