【C++11】智能指针
文章目录
1. 为什么需要智能指针?
我们先来分析下面这段程序有没有什么内存方面的问题?
提示一下:注意分析 MergeSort 函数中的问题。
代码如下所示:
#include <iostream>
using namespace std;
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
这段代码在异常发生时会发生内存泄漏。因为:new 分配的内存没有被 delete,而异常会导致函数提前退出。
但是明明代码里写了 delete,为什么还会泄漏?因为异常会 “跳过后面的代码”!
先看这段:
void Func()
{
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
你看到的是:后面不是有 delete p1; delete p2; 吗?但真实执行是:一旦异常发生,这两行根本不会执行!
如果 div() 抛异常了,也就是说假设 b == 0,执行:
throw invalid_argument("除0错误");
程序实际执行顺序如下:
1. p1 = new int ✅ 成功
2. p2 = new int ✅ 成功
3. 调用 div() ❌ 抛异常
4. 立刻离开 Func() ⛔(后面的代码全部跳过)
5. 进入 main 的 catch
而关键来了:
delete p1;
delete p2;
根本没执行到!!!
为什么会这样?这是 C++ 的异常机制:栈展开。
当异常发生时:当前函数立即停止执行,开始 “往外层函数返回”,沿途会调用栈上对象的析构函数,而不会执行普通语句(比如 delete)。
这句代码 int* p1 = new int; 中的 p1 是一个普通指针变量(栈上),但它指向的内存在堆上,异常时 p1 这个变量没了(栈清理),但它指向的堆内存没人管理,那么就会发生内存泄漏。
2. 内存泄漏
内存泄漏: 是指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害: 长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
2.1 内存泄漏分类
C/C++ 程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak): 堆内存指的是程序执行中依据须要分配通过 malloc / calloc / realloc / new 等从堆中分配的一块内存,用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生 Heap Leak。
系统资源泄漏: 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
2.3 如何避免内存泄漏
第一,工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
第二,采用 RAII 思想或者智能指针来管理资源。
第三,有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
第四,出问题了使用内存泄漏工具检测。(ps:不过很多工具都不够靠谱,或者收费昂贵。)
内存泄漏非常常见,解决方案分为两种:
- 事前预防型:如智能指针等。
- 事后查错型:如泄漏检测工具。
3. 智能指针的使用及原理
3.1 RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
大白话:资源获取以后马上进行初始化。就是说:你获取到了一个资源,但是你别自己去管理它,而是把它交给一个对象的生命周期来管。
这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
如下所示,我们可以使用 RAII 思想设计一个 SmartPtr 类
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete: " << _ptr << endl;
if (_ptr)
delete _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
运行结果如下:

这段代码通过自定义 SmartPtr,实现了一个 基于 RAII 思想的智能指针,用于管理动态内存,避免异常导致的内存泄漏。
RAII(资源获取即初始化)
SmartPtr<int> sp1(new int);
它会在构造函数中获取资源(new),在析构函数中释放资源(delete)。
当 div() 抛异常时:Func() 会提前退出,然后触发 栈展开,最后自动调用析构来释放 sp1 和 sp2 的内存。
但是目前的 SmartPtr 是简化版,存在一些隐患,如果在工程中的话是有很大问题的。
问题一:会发生重复释放
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2 = sp1; // 默认拷贝!
此时:
sp1._ptr ─┐
├──> 同一块内存
sp2._ptr ─┘
两个对象析构时:delete 同一块内存两次,从而导致崩溃。如下所示:

问题二:没有移动语义
现代 C++ 必须支持:移动构造 和 移动赋值,否则性能和安全都有问题。
下面是更安全的版本:
template<class T>
class SmartPtr
{
public:
// 构造
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
// 析构
~SmartPtr()
{
cout << "delete: " << _ptr << endl;
if (_ptr)
delete _ptr;
}
// 禁止拷贝和赋值
SmartPtr(const SmartPtr&) = delete;
SmartPtr& operator=(const SmartPtr&) = delete;
// 移动构造
SmartPtr(SmartPtr&& other)
: _ptr(other._ptr)
{
cout << "SmartPtr(SmartPtr&& other) --- 移动构造" << endl;
other._ptr = nullptr;
}
// 移动赋值
SmartPtr& operator=(SmartPtr&& other)
{
cout << "SmartPtr& operator=(SmartPtr&& other) --- 移动赋值" << endl;
if (this != &other)
{
delete _ptr;
_ptr = other._ptr;
other._ptr = nullptr;
}
return *this;
}
private:
T* _ptr;
};
此时我们再进行调试:

3.2 智能指针的原理
上述的 SmartPtr 还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过 -> 去访问所指空间中的内容,因此:SmartPtr 模板类中还得需要将 *、-> 重载一下,才可让其像指针一样去使用。
代码如下所示:
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{
public:
// 构造
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
// 析构
~SmartPtr()
{
cout << "delete: " << _ptr << endl;
if (_ptr)
delete _ptr;
}
// 禁止拷贝和赋值
SmartPtr(const SmartPtr&) = delete;
SmartPtr& operator=(const SmartPtr&) = delete;
// 移动构造
SmartPtr(SmartPtr&& other)
: _ptr(other._ptr)
{
cout << "SmartPtr(SmartPtr&& other) --- 移动构造" << endl;
other._ptr = nullptr;
}
// 移动赋值
SmartPtr& operator=(SmartPtr&& other)
{
cout << "SmartPtr& operator=(SmartPtr&& other) --- 移动赋值" << endl;
if (this != &other)
{
delete _ptr;
_ptr = other._ptr;
other._ptr = nullptr;
}
return *this;
}
// 重载 * 和 ->
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
// 日期类
struct Date
{
int _year;
int _month;
int _day;
};
int main()
{
SmartPtr<int> sp1(new int);
*sp1 = 10;
cout << "sp1=" << *sp1 << endl;
SmartPtr<Date> sp2(new Date);
// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
sp2->_year = 2026;
sp2->_month = 10;
sp2->_day = 5;
cout << sp2->_year << "/" << sp2->_month << "/" << sp2->_day << endl;
return 0;
}
运行结果如下:

总结一下智能指针的原理:
- RAII 特性
- 重载
operator*和operator->,使其具有像指针一样的行为。
4. auto_ptr
具体详情可以访问:auto_ptr 文档
C++98 版本的库中就提供了 auto_ptr 的智能指针,下面演示的 auto_ptr 的使用及问题。
auto_ptr 的实现原理:管理权转移的思想。
4.1 使用
代码如下所示:
// 自定义类
class A
{
public:
// 构造
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0) -- 构造" << endl;
}
// 析构
~A()
{
cout << "~A() -- 析构" << endl;
}
private:
int _a;
};
int main()
{
auto_ptr<A> ap1(new A(1));
return 0;
}
运行结果如下:可以看到此时 ap1 是能正常调用构造和析构的。

同样,如果要对 ap1 进行拷贝呢?
int main()
{
auto_ptr<A> ap1(new A(1));
// 拷贝
auto_ptr<A> ap2(ap1);
return 0;
}
运行结果如下:

可以看到 auto_ptr 很好的解决了拷贝的问题,但是它是怎么解决的呢?我们来调试看一下

我们发现 new A(1) 出来的资源最开始是由 ap1 进行管理的,现在用 ap1 去拷贝到 ap2 里面了,此时 ap1 已经悬空了,相当于把资源的管理权给了 ap2 了。这就是所谓的 管理权转移。
那么如果一个新手不懂这个,然后对 ap1 和 ap2 分别操作,就会发生崩溃。如下所示:

总结:所谓管理权转移,就是拷贝时会把拷贝对象的资源管理权转移给拷贝对象,那么就会导致拷贝对象悬空,此时访问就会出现问题。
4.2 模拟实现
下面我们简化模拟实现一份 auto_ptr 来了解它的原理。
代码如下所示:
// 自定义 auto_ptr
namespace edison
{
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;
}
}
// 析构
~auto_ptr()
{
if (_ptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
}
}
// 重载 * 和 ->
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
}
这段代码实现的是 “所有权转移”,也就是:**拷贝不是共享资源,而是把资源 “搬走” **
auto_ptr(auto_ptr<T>& sp)
: _ptr(sp._ptr)
{
sp._ptr = nullptr;
}
假设有:
auto_ptr<int> ap1(new int(10));
auto_ptr<int> ap2(ap1);
进入拷贝构造
: _ptr(sp._ptr)
此时:ap2._ptr = ap1._ptr,也就是说 ap1 和 ap2 指向同一块内存
接着,转移所有权
sp._ptr = nullptr;
这段代码会把 ap1 的指针置空,然后变成下面这样:
ap2._ptr -> 原来的内存
ap1._ptr -> nullptr
最终效果:ap2 接管资源,ap1 不再拥有资源。也就是说:
原来的拥有者放弃资源,新对象成为唯一拥有者
调试结果如下:

结论:auto_ptr 是一个失败设计,很多公司明确要求不能使用 auto_ptr。
5. unique_ptr
C++11 中开始提供更靠谱的 unique_ptr,具体详情可以访问:unique_ptr 文档
unique_ptr 的实现原理:简单粗暴的防拷贝。
5.1 使用
代码如下所示:
// 自定义类
class A
{
public:
// 构造
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0) --- 构造" << endl;
}
// 析构
~A()
{
cout << "~A() --- 析构" << endl;
}
//private:
int _a;
};
int main()
{
unique_ptr<A> up1(new A(10));
unique_ptr<A> up2(new A(20));
return 0;
}
运行结果如下:可以看到能够让对象正常的进行构造和析构

那么我们再进行拷贝一下呢?可以看到 unique_ptr 的解决方法非常简单粗暴,就是不允许你进行拷贝。

5.2 模拟实现
下面我们简化模拟实现一份 unique_ptr 来了解它的原理。
代码如下所示:
namespace edison
{
template<class T>
class unique_ptr
{
public:
// 构造函数
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;
private:
T* _ptr;
};
}
这两行代码在做什么?
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
作用只有一个:彻底禁止拷贝。也就是说:
unique_ptr<int> up1(new int(10));
unique_ptr<int> up2(up1); // 编译错误
up2 = up1; // 编译错误
为什么要禁止拷贝?先看如果 “允许拷贝” 会发生什么:
unique_ptr<int> up1(new int(10));
unique_ptr<int> up2 = up1;
如果是普通拷贝:
up1._ptr ─┐
├──> 同一块内存
up2._ptr ─┘
执行结果:
up1 析构 → delete
up2 析构 → 再 delete
问题:同一块内存被释放两次!
那么它和 auto_ptr 的核心区别是什么呢?
auto_ptr 的做法:
auto_ptr(auto_ptr<T>& sp)
{
_ptr = sp._ptr;
sp._ptr = nullptr;
}
它的特点是:允许拷贝,但拷贝时偷偷 “转移所有权”,会导致原对象被清空。
而 unique_ptr 的做法是直接禁止 = delete,意思是:这种操作从语法层面就不允许存在。
unique_ptr 的核心原则是:同一时刻只能有一个指针拥有资源。那怎么保证?
答案就是:
- 不允许拷贝(避免多个指针指向同一资源)
- 只允许 “转移所有权”(通过移动语义实现)
运行结果如下所示:

所以 unique_ptr 是禁止拷贝,保证资源永远只有一个拥有者。
6. shared_ptr
C++11 中开始提供更靠谱的、并且支持拷贝的智能指针,叫做 shared_ptr,具体详情可以访问:shared_ptr 文档
shared_ptr的原理:是通过引用计数的方式来实现多个 shared_ptr 对象之间共享资源。
例如:公司的行政人员晚上在下班之前都会通知,让最后走的同事记得把门锁下。
也就是说:
- shared_ptr 在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是 0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是 0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
6.1 使用
代码如下所示:
// 自定义类
class A
{
public:
// 构造
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0) --- 构造" << endl;
}
// 析构
~A()
{
cout << "~A() --- 析构" << endl;
}
//private:
int _a;
};
int main()
{
// C++11
shared_ptr<A> sp1(new A(10));
shared_ptr<A> sp2(new A(20));
// 拷贝
shared_ptr<A> sp3(sp1);
sp1->_a++;
sp3->_a++;
cout << "a = " << sp1->_a << endl;
cout << "a = " << sp3->_a << endl;
return 0;
}
运行结果如下所示:可以看到此时 pa1和 sp3 共同管理这份资源。

6.2 模拟实现
下面我们简化模拟实现一份 shared_ptr 来了解它的原理。
shared_ptr 是使用【引用计数】来支持多个拷贝管理同一个资源,最后一个析构对象释放资源。
namespace edison
{
template<class T>
class shared_ptr
{
public:
// 构造函数
shared_ptr(T* ptr)
:_ptr(ptr)
,_pConut(new int(1)) // 当有资源来的时候,就创建一个计数
{}
// 析构
~shared_ptr()
{
// 当计数为0的时候,就进行释放
if (--(*_pConut) == 0)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
delete _pConut;
}
}
// 重载 * 和 ->,能够像指针一样使用
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
// 拷贝构造
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pConut(sp._pConut)
{
++(*_pConut); // 对计数进行++操作
}
private:
T* _ptr;
int* _pConut; // 引用计数
};
}
如下所示:sp1 和 sp3 的引用计数为 2,sp2、sp4、sp5 的引用计数为 3。

那么画图演示如下:

其实对于自定义的 shared_ptr 最复杂的其实是赋值重载函数,也就是如果遇到 sp1 = sp5、sp3 = sp5 这种情况。因为它涉及 “旧资源释放 + 新资源共享 + 引用计数维护”。
参考代码如下所示:
// 赋值重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// _ptr != sp._ptr 防止自己给自己赋值
if (_ptr == sp._ptr)
{
return *this;
}
if (--(*_pConut) == 0)
{
delete _ptr;
delete _pConut;
}
_ptr = sp._ptr;
_pConut = sp._pConut;
++(*_pConut);
return *this;
}
我们一步一步解释这段代码到底在干什么。
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
它要完成三件事:
- 处理自己原来管理的资源
- 接管 sp 的资源
- 正确维护引用计数
第一步:防止自己给自己赋值
if (_ptr == sp._ptr)
{
return *this;
}
例如:
sp1 = sp1;
如果不判断:计数会先减 1,可能直接把资源释放掉,然后再使用已经被释放的指针,所以这里直接返回,避免灾难。
第二步:处理当前对象原来管理的资源
if (--(*_pConut) == 0)
{
delete _ptr;
delete _pConut;
}
假设当前对象是:
shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp2 = sp1;
此时:
sp1, sp2 -> 同一块内存
引用计数 = 2
如果执行:
sp1 = sp5;
那么 sp1 必须先:放弃原来的资源
步骤:
- 引用计数减一
- 如果变成 0,说明没人用了,那么释放资源即可。
第三步:接管新资源
_ptr = sp._ptr;
_pConut = sp._pConut;
现在:
sp1 和 sp5 指向同一块内存
第四步:更新新资源的引用计数
++(*_pConut);
因为:
sp1 = sp5;
意味着:原本 sp5 的资源被 1 个指针使用,现在变成 2 个指针使用,所以计数要 +1
假设:
shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp3(new int(30));
shared_ptr<int> sp5(new int(50));
执行:
sp1 = sp5;
sp3 = sp5;
第一次赋值:sp1 = sp5。
原状态:
sp1 -> 1 count=1
sp5 -> 5 count=1
如下所示:

执行后:
sp1 -> 5
sp5 -> 5
count = 2
如下所示:

第二次赋值:sp3 = sp5
原状态:
sp3 -> 3 count=1
sp1 -> 5
sp5 -> 5 count=2
如下图所示:

执行后:
sp3 -> 5
sp1 -> 5
sp5 -> 5
count = 3
如下图所示:

为什么顺序不能错?这段代码的顺序非常关键:
if (--(*_pConut) == 0)
delete _ptr;
必须先释放旧资源,再接管新资源。如果顺序反过来:
_ptr = sp._ptr;
--(*_pConut);
那么,你已经失去了原来资源的指针,再也无法 delete,会造成内存泄漏。
shared_ptr 的赋值运算符本质是:先减少旧资源的引用计数并在必要时释放,再共享新资源并增加其引用计数。
6.3 循环引用分析
shared_ptr 一般情况下是没有问题的,但是在一些极端场景下,会有一些缺点。
假设有下面这样一个场景:
struct Node
{
A _val;
Node* _next;
Node* _prev;
};
int main()
{
Node* n1 = new Node;
Node* n2 = new Node;
delete n1;
delete n2;
return 0;
}
目前这段代码是没有任何问题的

同样选用 shared_ptr 来管理对象,也是没有任何问题的

但是,如果我们想让 sp1 和 sp2 两个结点互相链接起来呢?同样还是会报错,这是一个经典的类型不匹配问题

很简单,我们 _next 和 _prev 用智能指针来定义
struct Node
{
A _val;
shared_ptr<Node> _next;
shared_ptr<Node> _prev;
};
int main()
{
// 智能指针
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
//
sp1->_next = sp2;
sp2->_prev = sp1;
return 0;
}
运行以后发现,程序倒是没有崩溃,但是 sp1 和 sp2 没有调用析构来进行释放!

初始状态:sp1 管理一个节点,sp2 管理一个节点
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
此时:不需要手动 delete,因为有 shared_ptr 管理。
sp1 所指节点:引用计数 = 1
sp2 所指节点:引用计数 = 1
接着建立双向关系
sp1->_next = sp2;
sp2->_prev = sp1;
但是发生了两件事:
- sp1 的 _next 持有 sp2 → sp2 引用计数 +1
- sp2 的 _prev 持有 sp1 → sp1 引用计数 +1
此时:
sp1 所指节点:引用计数 = 2(sp1 + sp2->_prev)
sp2 所指节点:引用计数 = 2(sp2 + sp1->_next)
那么 main 函数结束以后,sp1 和 sp2 析构
sp1 析构 → sp1 所指节点引用计数 -1 → 变成 1
sp2 析构 → sp2 所指节点引用计数 -1 → 变成 1
注意:此时两个节点都没有被释放!为什么没有释放?
因为:
sp1 节点 仍被 sp2->_prev 持有
sp2 节点 仍被 sp1->_next 持有
也就是说:每个节点的引用计数都还剩 1,所以析构函数不会触发 delete。
此时就形成 循环引用,关系如下:
Node1 (sp1) → _next → Node2
Node2 (sp2) → _prev → Node1
本质是:
Node1 持有 Node2
Node2 持有 Node1
为什么永远不会释放?关键点在这里:
- Node1 想释放 → 必须引用计数为 0,但它被 Node2 持有(_prev)。
- Node2 想释放 → 也必须引用计数为 0,但它被 Node1 持有(_next)。
结果 两者互相 “拉住对方”,谁也到不了 0。这不是普通的内存泄漏,而是 循环引用(cycle reference)
核心问题是:shared_ptr 是 “强引用”,会增加引用计数。当形成环时,引用计数永远无法归零,那么资源永远不会释放。
两个 shared_ptr 相互持有对方,会导致引用计数无法归零,从而产生循环引用,最终内存无法释放。
这里对应的标准解法就是:用 weak_ptr 来打破环,把节点中的 _prev 和 _next 改成 weak_ptr 就可以了。
如下所示:

6.3 定制删除器
所谓定制删除器,就是我们自己来定义控制删除资源的方式。
如果不是 new 出来的对象如何通过智能指针管理呢?比如 malloc 出来的对象。来看看下面这种情况。
代码如下所示:
// 自定义类
class A
{
public:
// 构造
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0) --- 构造" << endl;
}
// 析构
~A()
{
cout << "~A() --- 析构" << endl;
}
//private:
int _a;
};
int main()
{
shared_ptr<A> sp1((A*)malloc(10));
return 0;
}
运行结果如下:对于 malloc 申请的对象并没有进行 free。

并且除了 malloc,如果我们使用了 new [] 那么也会出现这种问题。
代码如下所示:
// 自定义类
class A
{
public:
// 构造
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0) --- 构造" << endl;
}
// 析构
~A()
{
cout << "~A() --- 析构" << endl;
}
//private:
int _a;
};
int main()
{
shared_ptr<A> sp1(new A[10]);
return 0;
}
运行结果如下:可以看到构造了 10 次,但是却析构了一次就崩溃了

其实 shared_ptr 设计了一个删除器来解决这个问题,我们可以先定制一个专门输出数组的删除器。
struct DeleteArray
{
void operator()(A* ptr)
{
delete[] ptr;
}
};
可以看到,此时就能成功的释放了

同样的,我们还可以把这个数组删除器写成一个类模板,这样所有类型都可以进行使用。
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
运行结果如下:

同时,对于 malloc 申请的资源,我们可以写一个仿函数的删除器。
// 仿函数的删除器
template<class T>
struct FreeFunc
{
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
运行结果如下所示:可以看到此时已经成功的释放掉了

同样,我们还可以写成 lambda 表达式的方式,这样更简洁
int main()
{
shared_ptr<A> sp1(new A[10], [](A* ptr) {delete[] ptr; });
shared_ptr<A> sp2((A*)malloc(10), [](A* ptr) {free(ptr); });
return 0;
}
同样,我们可以采用 function 函数包装器的方式,来为我们自己实现的 shared_ptr 增加删除器。
namespace edison
{
template<class T>
class shared_ptr
{
public:
// 构造函数
......
// 析构
~shared_ptr()
{
// 当计数为0的时候,就进行释放
if (--(*_pcount) == 0)
{
cout << "delete: " << _ptr << endl;
//delete _ptr;
_del(_ptr);
delete _pcount;
}
}
// 定制删除器
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
, _pConut(new int(1))
, _del(del)
{}
private:
T* _ptr;
int* _pConut; // 引用计数
function<void(T*)> _del; // 删除器
};
}
此时我们再换成自定义的 shared_ptr 来看看
// 自定义类
class A
{
public:
// 构造
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0) --- 构造" << endl;
}
// 析构
~A()
{
cout << "~A() --- 析构" << endl;
}
//private:
int _a;
};
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
edison::shared_ptr<A> sp1(new A[10], DeleteArray<A>());
return 0;
}
运行结果如下:

7. weak_ptr
具体详情可以访问:weak_ptr 文档
weak_ptr 原理:不拥有资源,只是 “观察者”,不会增加引用计数。
7.1 使用
weak_ptr 不是 RAII 智能指针,而是专门用来解决 shared_ptr 循环引用的问题。
它有一个函数叫做 use_count ,它可以用来查看引用计数。
struct Node
{
A _val;
weak_ptr<Node> _next;
weak_ptr<Node> _prev;
};
int main()
{
// 智能指针
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
cout << "链接前: " << "sp1.use_count=" << sp1.use_count() << " sp2.use_count= " << sp2.use_count() << endl;
//
sp1->_next = sp2;
sp2->_prev = sp1;
cout << "链接后: " << "sp1.use_count=" << sp1.use_count() << " sp2.use_count= " << sp2.use_count() << endl;
return 0;
}
运行结果如下:

本质机制:weak_ptr 指向 shared_ptr 的资源,但不参与 “所有权”,不参与资源管理,只做弱引用,用来打破 shared_ptr 的循环引用。
7.2 模拟实现
既然它是用来解决 shared_ptr 循环引用的问题,那么我们需要在自定义的 shared_ptr 中进行添加 use_count() 函数。
如下所示:
namespace edison
{
template<class T>
class shared_ptr
{
public:
// 构造函数
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pConut(new int(1)) // 当有资源来的时候,就创建一个计数
{}
// 析构
~shared_ptr()
{
// 当计数为0的时候,就进行释放
if (--(*_pConut) == 0)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
delete _pConut;
}
}
// 重载 * 和 ->,能够像指针一样使用
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
// 拷贝构造
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pConut(sp._pConut)
{
++(*_pConut); // 对计数进行++操作
}
// 赋值重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// _ptr != sp._ptr 防止自己给自己赋值
if (_ptr == sp._ptr)
{
return *this;
}
if (--(*_pConut) == 0)
{
delete _ptr;
delete _pConut;
}
_ptr = sp._ptr;
_pConut = sp._pConut;
++(*_pConut);
return *this;
}
// 统计引用计数的个数
int use_count()
{
return *_pConut;
}
// 获取原生指针
T* get()
{
return _ptr;
}
private:
T* _ptr;
int* _pConut; // 引用计数
};
}
struct Node
{
A _val;
edison::shared_ptr<Node> _next;
edison::shared_ptr<Node> _prev;
};
int main()
{
// 智能指针
edison::shared_ptr<Node> sp1(new Node);
edison::shared_ptr<Node> sp2(new Node);
cout << "链接前: " << "sp1.use_count=" << sp1.use_count() << " sp2.use_count= " << sp1.use_count() << endl;
//
sp1->_next = sp2;
sp2->_prev = sp1;
cout << "链接后: " << "sp1.use_count=" << sp1.use_count() << " sp2.use_count= " << sp2.use_count() << endl;
return 0;
}
运行结果如下:可以看到此时已经产生循环引用了。

那么接下来模拟实现一个 weak_ptr。
namespace edison
{
template<class T>
class weak_ptr
{
public:
// 无参构造
weak_ptr()
:_ptr(nullptr)
{}
// 用 shared_ptr 来构造
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
// 用 shared_ptr 来赋值
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
// 重载 * 和 ->,能够像指针一样使用
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
}
此时再用我们自己实现的 weak_ptr 来管理结点的 _next 和 _prev 指针
struct Node
{
A _val;
edison::weak_ptr<Node> _next;
edison::weak_ptr<Node> _prev;
};
运行结果如下:

可以看到我们自己实现的 weak_ptr 也能解决循环引用的问题!
8. 总结
我们来把智能指针总结一下:
auto_ptr:允许拷贝,但用 “置空源对象” 来转移所有权(隐式转移)
管理权转移,并且会导致被拷贝对象悬空,建议不要使用它。
unique_ptr:直接禁止拷贝,从语法层面杜绝多所有权(只允许唯一拥有)
禁止拷贝,简单粗暴,如果有不需要拷贝的场景,建议使用它。
shared_ptr:允许拷贝,通过 “引用计数” 实现多对象共享所有权
引用计数支持拷贝,如果碰到需要拷贝的场景,就使用它。但是要小心构成循环引用,因为循环引用会导致内存泄漏。
weak_ptr:不拥有资源,不增加引用计数,用于辅助 shared_ptr(解决循环引用)
专门解决 shared_ptr 的循环引用问题。
C++11 和 boost 库中智能指针的关系
C++ 98 中产生了第一个智能指针 auto_ptr,而 C++ 的 boost 库给出了更实用的 scoped_ptr 和 shared_ptr 和 weak_ptr。
接着 C++ 的 TR1 引入了 shared_ptr 等等,不过需要注意的是 TR1 并不是标准版。
最后在 C++ 11 中引入了 unique_ptr、shared_ptr 和 weak_ptr,不过需要注意的是 unique_ptr 对应的是 boost 库中的 scoped_ptr,并且这些智能指针的实现原理都是参考 boost 库来实现的。
智能指针完整代码
#include <iostream>
using namespace std;
namespace edison
{
template<class T>
class auto_ptr
{
public:
// 构造函数
auto_ptr(T* ptr)
:_ptr(ptr)
{}
// 赋值重载
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
}
// 析构
~auto_ptr()
{
if (_ptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
}
}
// 重载 * 和 ->,能够像指针一样使用
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
// 拷贝构造
// ap2(ap1)
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr; // 把 ap1 的内容给置空
}
private:
T* _ptr;
};
template<class T>
class unique_ptr
{
public:
// 构造函数
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;
private:
T* _ptr;
};
template<class T>
class shared_ptr
{
public:
// 构造函数
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1)) // 当有资源来的时候,就创建一个计数
{}
// 析构
~shared_ptr()
{
// 当计数为0的时候,就进行释放
if (--(*_pcount) == 0)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
// 重载 * 和 ->,能够像指针一样使用
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
// 拷贝构造
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount); // 对计数进行++操作
}
// 赋值重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// _ptr != sp._ptr 防止自己给自己赋值
if (_ptr == sp._ptr)
{
return *this;
}
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
// 统计引用计数的个数
int use_count() const
{
return *_pcount;
}
// 获取原生指针
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount; // 引用计数
};
template<class T>
class weak_ptr
{
public:
// 无参构造
weak_ptr()
:_ptr(nullptr)
{}
// 用 shared_ptr 来构造
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
// 用 shared_ptr 来赋值
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
// 重载 * 和 ->,能够像指针一样使用
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
}
以上就是 C++11 中智能指针的全部内容。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)