


目录
[3.1 核心定义](#3.1 核心定义)
[3.2 auto_ptr](#3.2 auto_ptr)
[3.3 unique_ptr](#3.3 unique_ptr)
[3.4 shared_ptr](#3.4 shared_ptr)
[3.4.1 循环引用](#3.4.1 循环引用)
[3.4.2 make_shared](#3.4.2 make_shared)
[3.4.3 线程安全问题](#3.4.3 线程安全问题)
[3.5 weak_ptr](#3.5 weak_ptr)
[4.1 unique_ptr](#4.1 unique_ptr)
[4.2 shared_ptr](#4.2 shared_ptr)

一、前言
智能指针是 C++11 标准引入的、用于自动化管理动态内存的工具,本质是封装了原始指针的类模板。它基于 RAII(资源获取即初始化) 机制,解决了原始指针手动管理内存易导致的内存泄漏、重复释放、野指针等问题,是现代 C++ 开发中管理堆内存的首选方案。
为什么引入智能指针呢?原始指针需要手动搭配 new/delete管理内存,极易出现问题:
如果忘记调用 delete 或者程序异常跳转导致我们写的delete未执行则会导致内存泄漏;重复释放同一块内存会导致程序崩溃;指向已释放内存的指针(野指针)会导致未定义行为。
比如这段代码:
cpp
double Divide(int a, int b)
{
//当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。
// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。
// 但是如果array2 new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案
// 是智能指针,否则代码太戳了
int* array1 = new int[10];
int* array2 = new int[10]; // 抛异常呢
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
cout << "delete []" << array1 << endl;
cout << "delete []" << array2 << endl;
delete[] array1;
delete[] array2;
throw; // 异常重新抛出,捕获到什么抛出什么
}
// ...
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
原生指针需要开发者手动调用 new分配、delete释放内存,但异常会彻底打断程序的正常执行流程,导致手动编写的 delete代码无法执行,直接造成内存泄漏。
在上述代码示例中主要有以下两种场景:
场景 1:业务逻辑抛出异常(除 0 错误) Func中 try 块调用 Divide,若输入除数为 0,Divide会抛出异常。如果没有手动添加 catch(...) 块捕获并释放,异常会直接跳出 Func函数,array1/array2后续的 delete代码完全不会执行,两块堆内存直接泄漏。代码中虽然加了 catch块手动释放,但这是 "补救式" 的,不是天然安全的。
景 2:内存分配本身抛出异常(new array2 失败) 代码中 new array1成功后,执行 new array2时,若系统内存不足,会抛出 std::bad_alloc 异常。此时异常发生在 try 块之前,不会进入 catch块,array1 已经分配的内存完全没有机会被 delete,直接永久泄漏。这种情况是原生指针的 天生缺陷:分配过程中出现异常,前面已经分配的资源无法自动回收。
二、RAII机制
RAII是ResourceAcquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是 ⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问, 资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
简单来讲RAII机制就是把需要手动管理的资源(内存、文件、锁、网络连接等),交给一个栈上的对象去托管:
- 对象创建(构造)时 → 自动申请 / 获取资源
- 对象销毁(析构)时 → 自动释放 / 关闭资源
因为 C++ 栈上的对象,离开作用域 {} 时,编译器会强制自动销毁(无论代码是正常结束、中途 return、还是抛出异常),所以资源永远不会泄漏。
三、智能指针
3.1 核心定义
智能指针 是 C++ 标准库定义于 <memory> 头文件中的类模板,其核心是通过 RAII(Resource Acquisition Is Initialization,资源获取即初始化) 机制封装原生指针,实现动态内存 / 资源的自动化生命周期管理,是现代 C++ 替代手动 new/delete 内存管理的标准方案。
智能指针本身是栈分配对象,利用 C++ 编译器自动调用栈对象析构函数的特性,在对象生命周期结束时自动释放托管的堆内存,从语言层面解决原生指针的内存泄漏、重复释放、野指针、异常不安全等问题。
C++ 标准定义四类智能指针,**std::auto_ptr**已废弃,现代 C++ 仅使用后三者:
| 类型 | 所有权语义 | 引用计数 | 性能 | 核心用途 |
|---|---|---|---|---|
std::auto_ptr |
独占(有缺陷) | 无 | 低 | C++98 遗留,C++11 弃用 |
std::unique_ptr |
独占式(强语义) | 无 | 零开销 | 独占资源管理(首选) |
std::shared_ptr |
共享式 | 有 | 轻微开销 | 多对象共享资源 |
std::weak_ptr |
非拥有式弱引用 | 无 | 零开销 | 解决 shared_ptr 循环引用 |
为什么要设计出这么多类型的智能指针呢?我们知道原生指针的拷贝操作默认是值拷贝(浅拷贝)多个指针变量可以存储完全相同的地址,指向同一块内存对象。换句话说,浅拷贝导致所有权被隐式复制这几个值相同的原生指针变量就同时拥有了同一块内存空间的"所有权",多个指针对该动态分配资源都拥有管理权、生命周期控制权,以及最终的释放责任。一个指针释放该资源后它的所有权失效不影响其他指针对同一块内存空间的所有权,他们也可以进行释放最终就会导致重复释放。
所以,原生指针无法回答:谁负责释放?谁是真正的管理者?
这就好比多个人拿着同一把钥匙,却没人规定谁最后锁门、谁负责销毁钥匙。有人先锁了门,其他人不知情还继续用钥匙开门(野指针访问),最后所有人都去锁一次门(重复释放),;更极端的是,所有人都忘了锁门,房子永远敞开(内存泄漏)。
而智能指针的核心设计思想,就是给指针绑定所有权规则,让内存的生命周期和指针的生命周期强绑定,离开作用域自动释放,同时用语法强制规定:同一时间,一块内存只能有唯一管理者,或计数共享管理者,绝对不允许无规则的浅拷贝。
但现实中的业务场景千差万别,我们不可能用一种规则适配所有需求 ------ 这就是必须设计多种智能指针的原因:
3.2 auto_ptr
auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给 拷贝对象,这是⼀个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计 出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用这个智能指针的。
cpp
#include <iostream>
#include <memory> // auto_ptr 头文件
using namespace std;
int main() {
// 1. 创建 auto_ptr,管理一块堆内存
auto_ptr<int> p1(new int(100));
// 2. 【关键】拷贝构造 p2,这一步会导致所有权转移!
auto_ptr<int> p2 = p1;
// 3. 致命问题:原对象 p1 已经悬空!访问会崩溃/未定义行为
cout << "p1 是否为空: " << (p1.get() == nullptr ? "是" : "否") << endl; // 是!
*p1; // 直接程序崩溃!野指针访问
return 0;
}

还有一个更加危险的场景就是auto_ptr给函数传值传参会导致隐形拷贝,原有的管理对象会被悬空
cpp
#include <iostream>
#include <memory>
using namespace std;
void func(auto_ptr<int> p) {
cout << "函数内: " << *p << endl;
} // 函数结束,p 销毁,内存被释放
int main() {
auto_ptr<int> p1(new int(200));
func(p1); // 隐形拷贝,p1 所有权转移,悬空!
*p1; // 这里访问 p1,程序直接崩溃!
return 0;
}

3.3 unique_ptr
unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,。它规定:一块动态内存,有且只有一个智能指针拥有它,绝对不允许拷贝(禁止浅拷贝),只能转移所有权(移动语义)。如果不需要拷贝的场景就非常建议使用他**。**
cpp
#include <memory>
using namespace std;
int main() {
unique_ptr<int> p1(new int(10));
// 错误!unique_ptr 禁止拷贝构造
unique_ptr<int> p2 = p1;
// 错误!禁止赋值
unique_ptr<int> p3;
p3 = p1;
cout << *p1 << endl; // 正常输出 10
}

当我们想要转移所有权的话可以使用移动语义:
cpp
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> p1(new int(20));
// 显式移动,转移所有权
unique_ptr<int> p2 = move(p1);
if (!p1) {
cout << "p1 已悬空" << endl;
}
if (p2) {
cout << "p2 接管: " << *p2 << endl;
}
}

3.4 shared_ptr
有些业务必须让多个指针共同管理同一块内存(比如多个对象引用同一个数据、缓存、多线程共享资源),不能强行独占。
所以C++11引入了智能指针shared_ptr,它的特点是允许拷贝,内部用引用计数解决共享所有权的释放问题
- 每拷贝一次,引用计数 +1;每销毁一个,计数 -1;
- 只有当计数变为 0(最后一个管理者销毁)时,才会真正释放内存;
- 彻底解决 谁最后释放的问题:最后一个走的人锁门。
这里重点要看看shared_ptr是如何设计的,尤其是引用计数的设计
1.设置为类中的普通成员变量
如果设置为类中普通成员变量,那么类实例化出的对象与对象之间各自都有自己的引用计数彼此之间不可见。此时引用计数就无法体现共享所有权的含义,智能指针对资源的管理与原生指针无异。

2.设置为类中的静态成员变量
如果设置为类中的静态成员变量,静态成员变量内存上只有一份,在全局区 / 静态区存储,其属于整个类,不属于任何一个对象。看似没问题但是如果一个sp原先指向资源1中途指向资源2了,那么此时资源1与资源2就同时被一个引用计数管理了:

主要这里一份资源就需要⼀个引用计数,所以引用计数才用静态成员的方式是无法实现的,要解决这个问题就需要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就要new一个引用计数出来。多个shared_ptr指向资源时就++引用计数,shared_ptr对象析构时就--引用计数,引用计数减到0时代表当前析构的shared_ptr是最后一个管理资源的对象,则析构资源。

share_ptr的模拟实现:
cpp
namespace yue
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
: _ptr(ptr)
, _pcount(new atomic<int>(1))
{}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp)
//不能给自己赋值
if (_ptr != sp._ptr)
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_pcount = sp._pcount;
_ptr = sp._ptr;
++(*_pcount);
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
private:
T* _ptr;
//int* _pcount;
atomic<int>* _pcount; // 原子操作
};
};
3.4.1 循环引用
shared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会使得资源没得到释放导致内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用weak_ptr解决这种问题。
下面我们举一个循环引用的例子:
cpp
#include<iostream>
#include<memory>
using namespace std;
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);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}


在这里我们定义了两个share_ptr指针分别管理两个双向链表节点,然后两个节点之间相互指向:

那么此时share_ptr类型的n1与n2析构之后,会导致引用计数从2减少到1不会使得资源被释放。这是因为两个资源之间还被彼此两个节点的前驱与后继指针指向,这时会导致两个资源谁也释放不了。因为其中一个节点的释放依赖另一个节点的释放(此时它的引用计数才能为0),而另一个节点的释放反过来也依赖这个节点的释放,形成了循环引用彼此共存的关系导致内存泄漏。

3.4.2 make_shared
std::make_shared是 C++11 标准库 <memory> 中专门用来创建并初始化对象,同时返回一个 std::make_shared。用于安全、高效构造 std::make_shared 的标准工厂函数,其设计目标是解决直接使用 new 配合 shared_ptr构造时存在的异常安全漏洞、内存效率低下、代码冗余等问题,是现代 C++ 中管理共享所有权堆对象的首选方案。
它是 创建 shared_ptr最推荐、最标准的写法。
我们讲过shared_ptr中的引用计数会开辟一块单独的内存空间,那么此时shared_ptr对象的内存与引用计数的内存可能不会开辟在一起就会导致内存碎片的问题。而make_shared就完美解决了这个问题:其只会申请一次内存,把对象内存 + 引用计数内存合并分配在一块比 shared_ptr<T>(new T)少一次内存分配**。**
3.4.3 线程安全问题
shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者 原子操作保证线程安全的。
shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr 管,它也管不了,应该有外层使用shared_ptr的人进行线程安全的控制。
cpp
struct AA
{
int _a1 = 0;
int _a2 = 0;
~AA()
{
cout << "~AA()" << endl;
}
};
int main()
{
yue::shared_ptr<AA> p(new AA);
const size_t n = 100000;
mutex mtx;
auto func = [&]()
{
for (size_t i = 0; i < n; ++i)
{
// 这⾥智能指针拷⻉会++计数
yue::shared_ptr<AA> copy(p);
{
unique_lock<mutex> lk(mtx);
copy->_a1++;
copy->_a2++;
}
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << p->_a1 << endl;
cout << p->_a2 << endl;
cout << p.use_count() << endl;
return 0;
}

3.5 weak_ptr
weak_ptr不支持RAII,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资 源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题。
weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的 shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr⽀持expired检查指向的 资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
cpp
#include<iostream>
#include<memory>
#include<string>
using namespace std;
int main()
{
std::shared_ptr<string> sp1(new string("111111"));
std::shared_ptr<string> sp2(sp1);
std::weak_ptr<string> wp = sp1;
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
// sp1和sp2都指向了其他资源,则weak_ptr就过期了
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
wp = sp1;
//std::shared_ptr<string> sp3 = wp.lock();
//这里wp调用lock会产生一个shared_ptr sp3与sp1共同管理string
auto sp3 = wp.lock();
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
*sp3 += "###";
cout << *sp1 << endl;
return 0;
}

用weak_ptr解决上面shared_ptr循环引用的代码如下:
cpp
#include<iostream>
#include<memory>
using namespace std;
struct ListNode
{
int _data;
//前驱与后继指针使用weak_ptr不会增加引用计数
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}

四、自定义删除器
智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指 针管理,析构时就会崩溃。智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器, 在智能指针析构时就会调用删除器去释放资源。
比如最常用的,当我们使用new[]开辟资源后就必须使用delete[]来释放,如果使用delete就会导致资源释放不完整内存泄漏的问题,当然编译器会检查出现这种情况会直接报错。因为new[]的情况比较常见所以在智能指针实现了一个特化版本,在这个版本中析构会自动调用delete[]而不是delete。
cpp
#include<memory>
using namespace std;
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
// 这样实现程序会崩溃
//unique_ptr<Date> up1(new Date[10]);
//shared_ptr<Date> sp1(new Date[10]);
//智能指针特化了一个版本,这里会调用delete[]
unique_ptr<Date[]> up1(new Date[3]);
shared_ptr<Date[]> sp1(new Date[3]);
return 0;
}

除了这些,有时候我们自己在对象中开辟了资源并将对象交给智能指针进行托管。或者是以下操作产生的资源:
malloc分配的内存- 文件指针
- 套接字、句柄、锁、共享内存等
- 需要特殊清理逻辑的对象
此时我们就必须给智能指针传递自定义删除器,当智能指针的生命周期结束会自动调用传进来的删除器完成资源的释放。
需要注意的是unique_ptr与shared_ptr都支持自定义删除器,但是他们的使用方法有所不同:unique_ptr 是在类模板参数支持的, shared_ptr 是构造函数参数支持的。下面我们来详细学习以下:
4.1 unique_ptr
在unique_ptr中删除器是模板参数是类型的一部分,语法格式:
cpp
template <class T, class Deleter = default_delete<T>>
class unique_ptr;
删除器的定义可以有三种方式,仿函数、函数指针与Lambda表达式。三者的传参方式如下:
1.函数指针
cpp
void free_int(int* p) {
free(p);
}
// 声明方式
std::unique_ptr<int, void(*)(int*)> ptr((int*)malloc(sizeof(int)), free_int);
2.仿函数
使用仿函数 unique_ptr 可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调用,这时与shared_ptr不同的地方之一。
cpp
struct FreeDeleter {
void operator()(int* p) const {
free(p);
}
};
std::unique_ptr<int, FreeDeleter> ptr((int*)malloc(4));
3.Lamdba
cpp
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
这里**decltype** 是 C++11 引入的类型推导关键字,它可以返回表达式的精确类型。它与auto的区别是**decltype** 会返回表达式的精确类型而auto可能会导致类型退化,所以这里使用**decltype**更准确。
4.2 shared_ptr
shared_ptr 的删除器不是模板参数,而是构造函数参数,存在控制块中。不需要再模板参数中指定删除器的类型而是在构造的时候直接传递:
1.函数指针
cpp
void free_int(int* p) {
free(p);
}
// 声明方式
std::shared_ptr<int> ptr((int*)malloc(sizeof(int)), free_int);
2.仿函数
使用仿函数 shared_ptr 必须在构造函数传递:
cpp
struct FreeDeleter {
void operator()(int* p) const {
free(p);
}
};
std::shared_ptr<int> ptr((int*)malloc(4),FreeDeleter());
3.Lamdba
cpp
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
shared_ptr<Date> up4(new Date[5], delArrOBJ);
//或者直接:
shared_ptr<Date> up4(new Date[5],[](Date* ptr) {delete[] ptr; } );
在上面的shared_ptr的模拟实现代码中我们没有加入删除器的部分,对于shared_ptr来讲用户传进来的删除其可能会有三种类型,所以要想将删除器加入进来就必须考虑类型统一的问题。C++11中的包装器就为我们提供了一种解决方法,包装器可以包装一切可调用对象包括Lambda,仿函数与函数指针消除类型不统一带来的影响。
如果用户不传递删除器那么删除器就会被默认初始化为delete:
cpp
namespace bit
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
: _ptr(ptr)
, _pcount(new atomic<int>(1))
{}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new atomic<int>(1))
, _del(del)
{}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
//delete _ptr;
_del(_ptr);
delete _pcount;
}
}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_pcount = sp._pcount;
_ptr = sp._ptr;
++(*_pcount);
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
private:
T* _ptr;
//int* _pcount;
atomic<int>* _pcount; // 原子操作
//删除器:
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
};