在 c 和 c++ 中,通过访问指针对象存储的地址,可以实现对内存的直接操作
但在实际工程中,由于复杂情况下意料外的程序跳转,程序很可能出现内存泄漏。
1 | int* p = new int(1); |
因此我们引入「智能指针」
智能指针的历史
智能指针是 RAII(Resource Acquistion Is Initialization)(资源分配就初始化)思想的产物
可以理解为:智能指针 = 指针 + RAII。
实际上, RAII 对于指针即为将指针封装为一个类,通过构造函数和析构函数管理指针,防止上述情况的内存泄漏。
一个典型的例子如下
1 | template <class T> |
作为一个智能指针,上述代码并不合格,至少还应实现赋值等操作。
auto_ptr
第一个被实现的智能指针 auto_ptr 于c++98被引入,实现了 RAII,通过「管理权转移|实现拷贝构造、赋值。
但 auto_ptr 并不被建议使用,其通过直接将堆上数据地址转移到新指针地址的方式实现拷贝构造、赋值,此时误访问原指针时很容易引起程序的崩溃。
参考以下代码:
1 | template<class T> |
针对上述缺陷, Boost 库引入 scoped_ptr ,后来也被 c++ 标准库(名为 unique )纳入。
scoped_ptr
scoped_ptr 通过限制拷贝构造和赋值运算的方式规避 auto_ptr 的缺陷。
具体:不对函数进行定义,使用 private 防止类外实现。
参考代码
1 | template<class T> |
scoped_ptr 终究是一个先天不全的智能指针。
目前的最终版本是 shared_ptr & weak_ptr。
shared_ptr
shared_ptr 是目前使用最广泛的智能指针,随 Boost 引入并在 C++11 被加入标准库
通过「引用计数」 shared_ptr 主要实现如下
1 | template<class T> |
循环引用问题
如下代码运行时,程序会出现无限循环问题,代码如下
1 | template<class T> |
如下图所示,在 main 函数结束时,两个智能指针生命周期结束时会依次析构,但在在两者交叉相互指向时,会陷入死循环中:
分析过程容易发现:
main函数执行到末尾时,计数器对 p1, p2 的计数都是2
p2 结束生命周期时,需要先析构 p2-> prev( p1 的空间,计数-1),再析构 p2 本身的空间,计数-1.
p1 结束生命周期时,需要先析构 p1-> next( p2 的空间,计数器将置0,需要 delete p2 )
但delete p2 需要先析构 p2-> prev 即 p1,p1 的析构又需要先析构 p1-> next 即delete p2
故而程序死循环。
循环引用解决方案 —> weak_ptr
1 | template<class T> |
此时,将循环引用问题中的节点定义中的智能指针从 shared_ptr 改为 weak_ptr 即可。
1 | //... |