
前言
作为 C++ 开发者,你是否曾因以下场景头疼不已?
- 函数中
new了数组,却因异常抛出导致后续delete没执行,排查半天定位到内存泄漏;- 多模块共享一块内存,不知道该由谁负责释放,最后要么重复释放崩溃,要么漏释放泄漏;
- 用了
auto_ptr后,拷贝对象导致原对象 "悬空",访问时直接崩溃却找不到原因。如果你有过这些经历,那智能指针一定是你必须掌握的现代 C++ 工具。它基于 RAII 思想,自动管理动态资源,让你无需手动
delete,从根源上减少内存泄漏风险。今天,我们就从 "为什么需要智能指针" 到 "不同智能指针的实战场景",带你系统掌握这一核心特性。
请君浏览
-
- 前言
- [一、智能指针的诞生:解决手动管理内存的 "千古难题"](#一、智能指针的诞生:解决手动管理内存的 “千古难题”)
-
- [1.1 一个典型的内存泄露场景](#1.1 一个典型的内存泄露场景)
- [1.2 智能指针的核心:RAII 思想](#1.2 智能指针的核心:RAII 思想)
- [二、C++ 标准库智能指针:4 种指针的特性与适用场景](#二、C++ 标准库智能指针:4 种指针的特性与适用场景)
-
- [2.1 auto_ptr:被淘汰的 "过渡品"(C++98)](#2.1 auto_ptr:被淘汰的 “过渡品”(C++98))
- [2.2 unique_ptr:不可共享的 "独占指针"(C++11)](#2.2 unique_ptr:不可共享的 “独占指针”(C++11))
- [2.3 shared_ptr:可共享的 "计数指针"(C++11)](#2.3 shared_ptr:可共享的 “计数指针”(C++11))
- [2.4 weak_ptr:解决循环引用的 "辅助指针"(C++11)](#2.4 weak_ptr:解决循环引用的 “辅助指针”(C++11))
- [2.5 删除器](#2.5 删除器)
- [3. shared_ptr的模拟实现](#3. shared_ptr的模拟实现)
-
- [3.1 原理](#3.1 原理)
- [3.2 代码](#3.2 代码)
- [4. 总结:智能指针的最佳实践](#4. 总结:智能指针的最佳实践)
- 尾声
一、智能指针的诞生:解决手动管理内存的 "千古难题"
在 C++ 中,内存泄漏的核心原因往往是 "资源申请与释放不匹配"------ 尤其是当程序流程被异常、分支跳转打断时,手动编写的delete可能永远不会执行。
**内存泄漏:**内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
**危害:**普通程序运⾏⼀会就结束了,出现内存泄漏问题也不大,进程正常结束,⻚表的映射关系解除,物理内存也可以释放。但⻓期运⾏的程序出现内存泄漏影响就很⼤了,如操作系统、后台服务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越慢,最终卡死。
1.1 一个典型的内存泄露场景
若函数中存在异常抛出,裸指针会因delete未执行导致泄漏,例如:我们在Func函数中new了两个数组,但如果Divide抛异常,后续的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()
{
int* array1 = new int[10];
int* array2 = new int[10];
//...
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
// ...
cout << "delete []" << endl;
delete[] array1;
delete[] array2;
}
int main()
{
try
{
Func();
}
catch (...)
{
cout << "abnormal" << endl;
}
return 0;
}
可以看到当Divide抛出异常时,我们new的两个数组就无法正常释放,导致内存泄漏:

即使我们加了try-catch,若new array2时本身抛异常,array1也无法释放,代码会变得臃肿且脆弱:
cpp
void Func()
{
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 []" << endl;
delete[] array1;
delete[] array2;
throw; // 异常重新抛出,捕获到什么抛出什么
}
// ...
cout << "delete []" << endl;
delete[] array1;
delete[] array2;
}
即便如此,因为new本⾝也可能抛异常,连续的两个new和下⾯的Divide都可能会抛异常,让我们处理起来很麻烦,这种场景下,我们需要一种 "能自动释放资源" 的机制 ------ 这就是智能指针的设计初衷。
1.2 智能指针的核心:RAII 思想
在 C++ 中,智能指针 是一种封装了裸指针(raw pointer )的模板类,其核心作用是自动管理动态内存 ,避免因手动调用delete疏忽导致的内存泄漏、重复释放或悬空指针等问题。它基于RAII(资源获取即初始化) 机制:在智能指针构造时获取资源(如动态内存),在析构时自动释放资源,无需手动干预。
智能指针的本质是RAII(Resource Acquisition Is Initialization,资源获取即初始化) 的实践:
- 资源(如动态内存、文件句柄)在智能指针对象构造时获取,并委托给该对象管理;
- 智能指针对象析构时自动释放资源,无论程序是正常结束还是异常退出(对象生命周期由作用域管理,析构总会执行);
- 为了方便使用,智能指针会重载
*、->、[]等运算符,模拟原生指针的行为。
基于此,我们可以先来自己简单粗略的实现一下智能指针,如下面代码所示:
cpp
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
: _ptr(ptr)
{}
~SmartPtr()
{
cout << "delete[] " << _ptr << endl;
delete[] _ptr;
}
// 重载运算符,模拟指针的⾏为,⽅便访问资源
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
};
有了智能指针,我们的func函数就可以改成:
cpp
void Func()
{
SmartPtr<int> sp1 = new int[10];
SmartPtr<int> sp2 = new int[10];
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
通过运行结果我们可以看到即便Divide函数抛出异常也不会影响我们new出来两个数组的释放:

虽然我们上面设计的智能指针是十分粗略的,但是可以看到即便如此也可以帮助我们解决内存泄漏的问题。那么接下来让我们看一看标准库中是如何设计智能指针的。
二、C++ 标准库智能指针:4 种指针的特性与适用场景
C++ 标准库(<memory>头文件)提供了 4 种智能指针,它们分别是auto_ptr、unique_ptr、shared_ptr、weak_ptr。其中除了auto_ptr外的三个都是在C++11中提出的。除weak_ptr外均遵循 RAII,原理上而⾔主要是解决智能指针拷⻉时的思路不同。下面来让我们看一看它们之间的区别,以及在不同场景下该如何选择。
2.1 auto_ptr:被淘汰的 "过渡品"(C++98)
auto_ptr是C++98时设计出来的智能指针,设计思路是 "拷贝时转移资源管理权"------ 但这是一个致命缺陷:拷贝后原对象会 "悬空"(资源指针被置空),后续访问原对象会触发空指针错误。如下面代码所示:
cpp
int main()
{
auto_ptr<Date> ap1(new Date); // ap1管理Date对象
auto_ptr<Date> ap2(ap1); // 拷贝:ap2获取管理权,ap1->_ptr被置空
// ap1->_year++; // 崩溃!ap1已悬空,访问空指针
return 0;
}
auto_ptr拷贝的原理是将自己的指针赋值给新的auto_ptr,并且使自己的指针置为空,这样我们再去访问这个对象时,就会因为访问空指针而导致报错。可以看到,当我们将ap1拷贝给ap2后,我们就访问ap1时就会报错,因为ap1已经悬空,我们不能去访问空指针。
正因如此,C++11 推出后,auto_ptr被明确标记为 "不推荐使用",多数公司的编码规范也会直接禁止它。
2.2 unique_ptr:不可共享的 "独占指针"(C++11)
unique_ptr(唯⼀指针)的设计思路是禁止拷贝、仅支持移动 ------ 确保同一时间只有一个unique_ptr管理资源,从根源上避免 "多个指针竞争释放" 的问题。它是 C++11 中最常用的智能指针之一,适用于 "资源无需共享" 的场景。
核心特性:
- 不可复制,只能移动:由于是独占所有权,
unique_ptr不支持复制构造或赋值(会编译报错),但可以通过std::move转移所有权(转移后原unique_ptr会失效,变为空指针)。 - 高效轻量:无额外引用计数开销,性能接近裸指针。
cpp
int main()
{
// 创建unique_ptr(推荐用make_unique,C++14起支持)
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::cout << *ptr1 << std::endl; // 输出:10
// 转移所有权(ptr1失效,ptr2拥有对象)
std::unique_ptr<int> ptr2 = std::move(ptr1);
if (ptr1 == nullptr) {
std::cout << "ptr1已失效" << std::endl; // 输出:ptr1已失效
}
// 超出作用域时,ptr2析构,自动释放内存
return 0;
}
适用场景:管理独占资源(如局部动态对象、类的成员变量),作为函数返回值(无需手动释放,避免返回裸指针的风险)。
2.3 shared_ptr:可共享的 "计数指针"(C++11)
shared_ptr(共享指针)允许多个 shared_ptr 共同拥有同一个动态对象。。也就是说支持资源共享,其核心是通过 "引用计数" 跟踪管理资源的指针数量:
- 当新的
shared_ptr拷贝或赋值时,引用计数+1; - 当
shared_ptr析构时,引用计数-1; - 当引用计数减至
0时,代表当前是最后一个管理资源的指针,自动释放资源。
核心特性:
- 引用计数透明管理:用户无需手动维护计数,
use_count()方法可查看当前计数; - 支持拷贝与移动:拷贝时计数
+1,移动时计数不变(原对象悬空);
cpp
int main() {
// 创建shared_ptr(推荐用make_shared,更高效)
std::shared_ptr<int> ptr1 = std::make_shared<int>(20);
std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:1
// 复制ptr1,引用计数+1
std::shared_ptr<int> ptr2 = ptr1;
std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:2
// ptr2超出作用域,引用计数-1
{
std::shared_ptr<int> ptr3 = ptr1;
std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:3
}
std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:2
// 最后ptr1和ptr2析构,引用计数变为0,内存释放
return 0;
}
shared_ptr的构造有两种方式,除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值直接构造:
-
shared_ptr<Date> sp(new Date(2024, 10, 1));:两次内存分配(一次给Date对象,一次给引用计数); -
auto sp = make_shared<Date>(2024, 10, 1);:一次内存分配(同时存储Date对象和引用计数),效率更高,且避免内存泄漏风险(若new成功但计数分配失败,new的对象无法释放)。cpptemplate <class T, class... Args> shared_ptr<T> make_shared(Args&&... args);
对于shared_ptr和unique_ptr,我们还需要注意下面几点:
-
shared_ptr和unique_ptr都⽀持了operator bool的类型转换:如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。 -
shared_ptr和unique_ptr的构造函数都使⽤explicit修饰,防⽌普通指针隐式类型转换成智能指针对象。cpp// 报错:无法进行隐式类型转换 shared_ptr<Date> sp5 = new Date(2024, 9, 11); unique_ptr<Date> sp6 = new Date(2024, 9, 11);
使用
shared_ptr还要注意线程安全问题,shared_ptr的引⽤计数对象在堆上,如果多个shared_ptr对象在多个线程中,进⾏shared_ptr的拷贝和析构时会访问修改引⽤计数,就会存在线程安全问题,所以shared_ptr引⽤计数是需要加锁或者原⼦操作保证线程安全的。
相关文档shared_ptr
2.4 weak_ptr:解决循环引用的 "辅助指针"(C++11)
weak_ptr(弱指针)完全不同于上⾯的智能指针,是一个特殊的智能指针。它不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_ptr的⼀个循环引⽤导致内存泄漏的问题。
什么是循环引用呢?shared_ptr 的引用计数机制可能导致循环引用问题:两个对象互相持有对方的 shared_ptr,此时它们的引用计数永远不会变为 0,导致内存泄漏。
例如:我们有两个链表结点,把它们分别交给智能指针shared_ptr管理,然后将它们连接起来,如下面代码所示:
cpp
struct ListNode
{
int _data;
shared_ptr<ListNode> _next; // 指向后一个节点
shared_ptr<ListNode> _prev; // 指向前一个节点
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl; // 1
cout << n2.use_count() << endl; // 1
n1->_next = n2; // n2的计数+1 → 2
n2->_prev = n1; // n1的计数+1 → 2
// 析构n1和n2:计数各减1 → 1(而非0)
// 节点资源永远无法释放,内存泄漏!
return 0;
}
循环引用的逻辑链:n1->_next依赖n2释放,n2->_prev依赖n1释放,最终谁都无法释放。

那么该如何解决循环引用呢?这时候weak_ptr就派上用场了。weak_ptr 是一种弱引用 智能指针,它不拥有对象的所有权,也不会增加引用计数,因此我们修改链表节点为weak_ptr后,循环引用被打破:
cpp
struct ListNode
{
int _data;
weak_ptr<ListNode> _next; // 改为weak_ptr,不增加计数
weak_ptr<ListNode> _prev;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2; // 不增加n2的计数(仍为1)
n2->_prev = n1; // 不增加n1的计数(仍为1)
// 析构n1和n2:计数各减1 → 0,节点资源正常释放
return 0;
}
weak_ptr不⽀持RAII,也不⽀持访问资源,所以weak_ptr构造时不⽀持绑定到资源,只⽀持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,那么就可以解决上述的循环引⽤问题。

weak_ptr的核心特性是:绑定到shared_ptr时不增加引用计数 ,仅作为 "观察者" 跟踪资源是否有效。weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理 。那么如果它绑定的shared_ptr已经释放了资源,那么它去访问资源就是很危险的,为此它提供了两个关键方法:
expired():判断绑定的shared_ptr资源是否已释放(计数为 0);lock():若资源有效,返回一个shared_ptr(计数 + 1,安全访问资源);若无效,返回空shared_ptr。
cpp
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();
auto sp3 = wp.lock();
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
*sp3 += "###";
cout << *sp1 << endl;
return 0;
}
weak_ptr仅作为shared_ptr的辅助工具,解决循环引用(如链表、树、图等数据结构的节点引用)。
2.5 删除器
智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。**因此当管理非new资源(如new[]、文件指针)时,需自定义删除器。**智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤对象,在这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调⽤删除器去释放资源。因为new[]经常使⽤,所以为了简洁,unique_ptr和shared_ptr都特化了⼀份[]的版本:

cpp
int main()
{
// 这样实现程序会崩溃
// unique_ptr<Date> up1(new Date[10]);
// shared_ptr<Date> sp1(new Date[10]);
// 因为new[]经常使⽤,所以unique_ptr和shared_ptr
// 实现了⼀个特化版本,这个特化版本析构时⽤的delete[]
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
return 0;
}
除此之外,我们还可以自定义删除器,这里我们有三种方式:
-
仿函数
cpptemplate<class T> class DeleteArray { public: void operator()(T* ptr) { delete[] ptr; } }; unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]); shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>()); -
函数指针
cpptemplate<class T> class DeleteArray { public: void operator()(T* ptr) { delete[] ptr; } } unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>); shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>); -
lambda表达式
cppauto delArrOBJ = [](Date* ptr) {delete[] ptr; }; unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ); shared_ptr<Date> sp4(new Date[5], delArrOBJ);
我们可以看到,使用不同的可调用对象,unique_ptr和shared_ptr需要传入的参数也是不同的,这是因为unique_ptr和shared_ptr⽀持删除器的⽅式有所不同:
unique_ptr是在类模板参数⽀持的;shared_ptr是构造函数参数⽀持的。
这⾥没有使⽤相同的⽅式还是挺不方便,也是标准库中的一点小弊端。
使⽤仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调用,但是函数指针和lambda表达式的类型是不可以的,所以在传参时不仅要在模板传类型,还要在构造传入相应的对象。
3. shared_ptr的模拟实现
想要加深对智能指针的印象,我们可以自己来模拟实现一下智能指针,在标准库中的四种智能指针中shared_ptr涉猎最广,所以我们来模拟实现一下shared_ptr,当然我们这里只是简单的模拟,标准库中的shared_ptr的实现是极为复杂的。
3.1 原理
实现shared_ptr我们需要搞定两个比较重要的东西,其中之一是引用计数的设计,主要这⾥⼀份资源就需要⼀个引⽤计数,所以引⽤计数采⽤静态成员的⽅式是⽆法实现的,要使⽤堆上动态开辟的⽅式,构造智能指针对象时来⼀份资源,就要new⼀个引⽤计数出来。多个shared_ptr指向资源时就++引⽤计数,shared_ptr对象析构时就--引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀个管理资源的对象,则析构资源。

其次就是删除器,我们要在构造时传入删除器,但是在析构时才会使用删除器,也就是说我们需要将删除器保存为成员函数,这样才能在析构时去调用,那么我们该如何保存删除器呢?我们知道函数指针、仿函数、lambda表达式这些都可以做删除器,这时候我们就需要用到function包装器(详情点击)了,通过包装器来存储删除器,这样就可以存储不同的删除器了:function<void(T*)> _del = [](T* ptr) {delete ptr; };
3.2 代码
下面让我们来看具体的代码实现:
cpp
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _del(del)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _del(sp._del)
{
++(*_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();
}
//获得原生指针
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
需要注意的是我们这⾥实现的shared_ptr是以最简洁的⽅式实现的,只能满⾜基本的功能。感兴趣的可以去查看源代码。
4. 总结:智能指针的最佳实践
掌握智能指针后,我们可以从 "手动管理内存" 的焦虑中解放出来。以下是核心实践原则:
- 优先用 unique_ptr :若资源无需共享,
unique_ptr是最高效的选择(无引用计数开销); - 共享用 shared_ptr :需多模块共享资源时用
shared_ptr,优先用make_shared优化; - 循环引用用 weak_ptr :链表、树等结构中,节点间引用用
weak_ptr避免泄漏; - 自定义删除器 :管理
new[]、文件句柄等非new资源时,务必指定删除器; - 避免裸指针混用:尽量不要用智能指针管理 "已被裸指针管理的资源",避免重复释放。
最后记住:智能指针不是 "银弹",但它是现代 C++ 中避免内存泄漏的最有效工具。用好智能指针,让你的代码更安全、更优雅!
尾声
若有纰漏或不足之处欢迎大家在评论区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!