c++智能指定到底是什么?看完彻底搞懂auto_ptr/unique_ptr/shared_ptr/weak_ptr
前言:为什么需要智能智能?
先看一段很普通的代码
double Divide(int a, int b)
{
try
{
if (b == 0)
{
string s("Divide by zero condition!");
throw s;
}
else
{
return ((double)a / (double)b);
}
}
catch (const int& s)
{
cout << s << endl;
}
cout << __FUNCTION__ << ":" << __LINE__ << "继续执行" << endl;
return 0;
}
这里 catch (const int&) 并不能捕获 string 类型的异常,所以 Divide 中抛出的异常会继续向上层传播。
如果按照传统写法在 Func 中申请两块内存:
void Func()
{
int* array1 = new int[10];
int* array2 = new int[10]; // 如果这行抛异常,array1 就泄漏了
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;
}
这段代码至少有三个问题:
-
Divide抛异常后,后面的delete[]不会执行,内存泄漏; -
第一个
new成功、第二个new抛异常时,array1还没进入try,同样泄漏; -
每多申请一份资源,就要多写一套
catch释放逻辑,代码又长又容易漏。
结果就是:明明 new 了也 delete 了,程序却仍然可能泄漏。 智能指针要解决的正是这类"资源释放必须依赖控制流"的问题。
一、RAII:智能指针的设计思想
RAII 是 Resource Acquisition Is Initialization 的缩写,即"资源获取即初始化"。它的核心不是"智能",而是把资源交给对象的生命周期管理:
-
对象构造时获取资源;
-
对象存活期间资源始终有效;
-
对象析构时自动释放资源。
这里的资源不限于堆内存,还包括文件指针、网络连接、互斥锁等。只要资源的生命周期能和一个栈对象绑定,就能享受"离开作用域自动释放"的待遇,即使中途抛异常,栈展开时析构函数也一定会被调用。
对于上面代码段可能出现内存泄漏的问题,这里就可以借助RAII 的思想来实现一个智能指针管理数组的对象,其他RAII的核心思想就是:构造时获取资源,析构函数自动释放资源,依靠对象生命周期管理资源。代码实现如下:
template<class T>
class smart_ptr
{
public:
smart_ptr(T* ptr)
: _ptr(ptr)
{ }
~smart_ptr()
{
cout << "delete[] _ptr" << endl;
delete[] _ptr;
}
T& operator[](const size_t i)
{
return _ptr[i];
}
T& operator*()
{
return *_ptr;
}
T* operator&()
{
return _ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
它做了两件事:
-
构造时接管裸指针,析构时释放,这是 RAII;
-
重载
*、->、[]、&,让对象用起来仍然"像指针"。
于是 Func 可以简化为:
void Func()
{
// 智能指针管控资源,此时程序不会因为异常导致资源泄漏
smart_ptr array1 = new int[10];
smart_ptr array2 = new int[10];
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
无论 Divide 是否抛异常、第二个 new 是否成功,只要 array1、array2 构造完成,离开 Func 时析构函数就会自动释放资源。
不过这个简易的智能指针还有明显缺陷:
-
固定使用
delete[],无法管理单个对象、FILE*等资源; -
没有处理拷贝:默认拷贝只是浅拷贝
_ptr,两个对象析构时会对同一块内存delete[]两次; -
没有移动语义,也没有引用计数。
"拷贝时资源归谁"正是各种智能指针的分水岭 ,由此引出了 auto_ptr、unique_ptr、shared_ptr 和 weak_ptr。
二、C++ 标准库中的智能指针
标准库智能指针都在 <memory> 中。除了 weak_ptr,它们都符合 RAII,并且像指针一样支持 *、-> 等操作。它们的主要区别在于拷贝时的所有权处理方式。
| 智能指针 | 引入标准 | 所有权 | 拷贝 | 移动 | 引用计数 |
|---|---|---|---|---|---|
auto_ptr |
C++98 | 独占 | 拷贝即转移 | --- | 无 |
unique_ptr |
C++11 | 独占 | 禁止 | 支持 | 无 |
shared_ptr |
C++11 | 共享 | 支持 | 支持 | 有 |
weak_ptr |
C++11 | 不拥有 | 可从 shared_ptr 构造 |
支持 | 不增加计数 |
1. auto_ptr:拷贝即转移的失败设计
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1); // ap1 的资源被转移给 ap2,ap1 变成悬空指针

auto_ptr 在拷贝时把资源管理权直接转走,被拷贝对象随即悬空。一旦再访问 ap1,就是未定义行为。这个设计过于危险,C++11 已将其弃用,实际项目中应完全避免。
2. unique_ptr:独占所有权,只移不拷
unique_ptr 的名字就是"唯一指针":同一时刻只能有一个 unique_ptr 拥有资源。
unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1); // 错误:拷贝构造被删除
unique_ptr<Date> up2(move(up1)); // 正确:转移所有权
所以在一些不用拷贝的场景,就很适合用unique_ptr 智能指针。unique_ptr 开销极小,内部就一个裸指针,几乎和原生指针性能一模一样。
3.shared_ptr:共享智能指针
共享的意思就是和其他人一起共享资源,所以shared_ptr 即支持拷贝构造,实现多个对象管理一个资源块,也支持移动构造。
底层是如何实现不同对象管理同一个资源,要释放的又是怎么做到不重复释放的。
解决思路:增加一块计数器(控制块),记录有多少个 shared_ptr 在管理这块资源。
- 新建 / 拷贝一份 shared_ptr:计数器 + 1
- 析构 / 放弃资源:计数器 - 1
- 计数器减到 0 的时候,才真正 delete 释放内存
内存布局:
- shared_ptr 对象内部两样东西:
- 资源裸指针(指向你 new 的对象)
- 指向控制块的指针,控制块堆上分配,里面存引用计数、自定义删除器。
多个 shared_ptr,对象本身是不同的,但是指向同一份控制块!
shared_ptr<int> s1(new int(100));
cout << s1.use_count() << endl; // 1
shared_ptr<int> s2 = s1; // 拷贝,计数+1
cout << s1.use_count() << endl; //2
cout << s2.use_count() << endl; //2
s1.reset(); // s1放弃资源,计数-1
cout << s2.use_count() << endl; //1
s2.reset(); // 计数变成0,内存释放
运行结果:

补充:make_shared() 函数
template <class T, class... Args> shared_ptr<T> make_shared (Args&&... args);
make_shared是 C++ 提供的工厂函数,一次性分配一块连续内存存放目标对象和shared_ptr 的控制块推荐:
auto s = make_shared<int>(100);构造shared_ptr对象。// 等价 shared_ptr<Date> sp(new Date(2026,10,21));
auto sp = make_shared<Date>(2026, 10, 21);
4.weak_ptr:只观察,不拥有
weak_ptr 不参与 RAII,不能直接管理资源,它必须从 shared_ptr 构造或赋值。它的特点是不增加引用计数,因此不会影响资源的释放。
std::shared_ptr<string> sp1(new string("111111"));
std::shared_ptr<string> sp2(sp1);
std::weak_ptr<string> wp = sp1;
cout << wp.expired() << endl; // 0,资源仍然存在
cout << wp.use_count() << endl; // 2
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl; // 0,sp2 还持有旧资源
cout << wp.use_count() << endl; // 1
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl; // 1,旧资源已释放
cout << wp.use_count() << endl; // 0
wp = sp1;
auto sp3 = wp.lock(); // 提升为 shared_ptr,计数加一
sp1.reset();
cout << wp.expired() << endl; // 0,sp3 仍持有
cout << wp.use_count() << endl; // 1
*sp3 += "###";
cout << *sp3 << endl; // 222222###
lock() 的语义是:如果资源还活着,就返回一个有效的 shared_ptr;否则返回空。访问 weak_ptr 管理的对象前,必须先 lock(),不能直接解引用,否则资源可能已经被释放。
weak_ptr的产生主要是为了解决shared_ptr循环引用导致内存泄露的场景,在后面我们还会介绍。
三、定制删除器
shared_ptr 和 unique_ptr 默认都用 delete 释放资源。但资源不一定是 new 出来的:
-
new[]需要delete[]; -
FILE*需要fclose; -
malloc需要free; -
网络连接、锁等需要专门的释放函数。
这时就要传入定制删除器。删除器本质是一个可调用对象,可以是仿函数、函数指针或 lambda。
我们先看库里是如何解决这个问题的?


unique_ptr删除器:模板参数,类型一部分shared_ptr删除器:构造函数参数,不参与模板类型
使用方法:
// ========== 错误写法(放开会崩溃!)==========
// unique_ptr<Date> up1(new Date[10]);
// shared_ptr<Date> sp1(new Date[10]);
// 原因:默认deleter是delete,不是delete[],数组只调用第一个元素析构,内存释放错乱
// ========== 解决方案1:使用数组特化 unique_ptr<T[]> / shared_ptr<T[]> (C++17) ==========
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
cout << "===== 数组特化版本 unique_ptr<T[]>、shared_ptr<T[]> =====" << endl;
// ========== 解决方案2:自定义删除器 仿函数 ==========
// 仿函数支持:DeleteArray无参构造,可以省略第二个参数
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
// shared_ptr 删除器是构造参数,必须传入删除器对象
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
cout << "===== 仿函数删除器 =====" << endl;
// ========== 解决方案2:函数指针作为删除器 ==========
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
cout << "===== 函数指针删除器 =====" << endl;
// ========== 解决方案2:lambda作为删除器 ==========
auto delArrOBJ = [](Date* ptr) {
cout << "【lambda删除器】delete[] 数组" << endl;
delete[] ptr;
};
// lambda类型用decltype拿到,unique_ptr模板要带上删除器类型,构造必须传入lambda对象
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
cout << "===== lambda删除器 =====" << endl;
// ========== 其他资源:FILE 文件句柄管理 ==========
// 方式1:lambda删除器(推荐,直观,可以加日志)
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
if (ptr)
{
cout << "【lambda】调用fclose, ptr=" << ptr << endl;
fclose(ptr);
}
});
// 方式2:直接传入标准fclose函数指针
shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), fclose);
四、智能指针的原理
下面我们模拟实现了unique_ptr的核心功能,这个智能指针的实现比较简单,我们了解一下原理即可。auto_ptr的思路是拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,也不建议使用。unique_ptr的思路是不支持拷贝。
重点要看看shared_ptr是如何设计的,尤其是引用计数的设计,主要这里一份资源就需要一个
引用计数,所以引用计数才用静态成员的方式是无法实现的,要使用堆上动态开辟的方式,构造智
能指针对象时来一份资源,就要new一个引|用计数出来。多个shared_ptr指向资源时就++引用计
数,shared_ptr对象析构时就-引用计数,引用计数减到0时代表当前析构的shared_ptr是最后一
个管理资源的对象,则析构资源。
namespace gxy
{
template<class T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr)
: _ptr(ptr)
{ }
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
// 禁止拷贝
unique_ptr(unique_ptr<T>& ptr) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& ptr) = delete;
// 允许移动:直接"窃取"对方的指针
unique_ptr(unique_ptr<T>&& ptr)
: _ptr(ptr._ptr)
{
ptr._ptr = nullptr;
}
T& operator[](const size_t i) { return _ptr[i]; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
}
关键点:
拷贝构造和拷贝赋值用
= delete禁用,从编译期杜绝"两个对象管同一块资源";移动构造把
_ptr取走,并把源对象置空,保证析构时只有一个对象真正释放;不需要引用计数,因此开销和裸指针几乎一样。
只要场景不需要共享所有权,
unique_ptr应该是默认选择。
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, count(new int(1))
{ }
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, count(new int(1))
, _del(del)
{ }
// 拷贝构造:共享指针和计数,计数加一
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, count(sp.count)
, _del(sp._del)
{
(*count)++;
}
// 移动构造:窃取资源
shared_ptr(shared_ptr<T>&& sp)
: _ptr(sp._ptr)
, count(sp.count)
, _del(sp._del)
{
sp._ptr = nullptr;
}
void release()
{
// 最后一个持有资源的对象负责释放
if (--(*count) == 0)
{
_del(_ptr);
delete count;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (this != &sp)
{
release(); // 先放弃自己原来持有的资源
_ptr = sp._ptr;
count = sp.count;
_del = sp._del;
(*count)++; // 再共享新资源
}
return *this;
}
shared_ptr<T>& operator=(shared_ptr<T>&& sp)
{
if (this != &sp)
{
release();
_ptr = sp._ptr;
count = sp.count;
_del = sp._del;
sp._ptr = nullptr;
}
return *this;
}
~shared_ptr()
{
release();
}
T* get() const { return _ptr; }
int use_count() const { return *count; }
T& operator[](const size_t i) { return _ptr[i]; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr = nullptr;
int* count = nullptr; // 计数必须放在堆上,才能被所有拷贝共享
function<void(T*)> _del = [](T* ptr) { delete ptr; };
};
几个容易忽略的细节:
计数为什么用
int*而不是int?如果用
int count,每个shared_ptr拷贝都有自己的计数,根本没法共享。必须让所有副本指向同一块堆上的计数 ,所以用new int(1),析构时再delete count。拷贝构造只加计数,不拷贝资源,指向同一块资源。
赋值为什么先
release()?假设
sp1原来指向 A,现在要指向 B:
先放弃 A,A 的计数减一,若归零就释放 A;
再指向 B,并让 B 的计数加一。
顺序反了就可能漏释放,或者自己给自己赋值时把计数搞乱,所以还要判断
this != &sp。移动构造要"掏空"源对象。
源对象把指针交出去后应置空,避免析构时重复释放。
定制删除器的设置(模仿标准 shared_ptr 设计)
我们想写一个模板构造函数,支持传入任意形式的删除器(仿函数、lambda、函数指针都行)。但有一个棘手问题:不同删除器是完全不同的类型,而且这个删除器只是构造函数的形参,构造结束后,析构函数拿不到它、没法调用。
标准库的思路就是用
std::function做类型擦除 :在 shared_ptr 内部保存一个std::function包装对象。构造智能指针的时候,把外面传进来的删除器打包存进这个 function。析构的时候不需要关心原来删除器是什么类型,直接调用function,就可以执行对应的资源释放逻辑。
五、循环引用:shared_ptr 的经典陷阱
shared_ptr 最典型的问题就是循环引用。看下面的一段双向链表的节点的代码
struct ListNode
{
int _data;
// gxy::shared_ptr<ListNode> _next;
// gxy::shared_ptr<ListNode> _prev;
gxy::weak_ptr<ListNode> _next;
gxy::weak_ptr<ListNode> _prev;
ListNode(int val = 0)
: _data(val)
{ }
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
gxy::shared_ptr<ListNode> n1(new ListNode(1));
gxy::shared_ptr<ListNode> n2(new ListNode(2));
cout << n1.use_count() << endl; // 1
cout << n2.use_count() << endl; // 1
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl; // 1
cout << n2.use_count() << endl; // 1
return 0;
}
如果把成员改成 shared_ptr:
struct ListNode
{
int _data;
gxy::shared_ptr<ListNode> _next;
gxy::shared_ptr<ListNode> _prev;
// ...
};
执行
n1->_next = n2;后,n2的计数从 1 变成 2;执行n2->_prev = n1;后,n1的计数也从 1 变成 2。main结束时:
n1析构,计数 2 → 1;
n2析构,计数 2 → 1。两个计数都无法归零,于是两个节点永远不会被释放,
~ListNode()也不会执行,这就是循环引用导致的内存泄漏。改成
weak_ptr后,_next、_prev只是"观察"对方,不增加计数,计数始终保持 1。n1、n2析构时计数归零,节点正常释放,~ListNode()会被调用两次。由此可以总结出
weak_ptr的典型用途:
打破
shared_ptr的循环引用;观察者/回调中引用对象,但不延长其生命周期;
缓存中保留弱引用,需要时再
lock()提升;父子结构中,父持有子的
shared_ptr,子只持有父的weak_ptr。
六、总结
智能指针的核心可以概括为一句话:用对象的生命周期管理资源,用所有权模型管理拷贝。
-
RAII 解决了"异常路径下资源漏释放"的问题;
-
unique_ptr 用"禁止拷贝、只允许移动"表达独占所有权,零额外开销,应作为默认选择;
-
shared_ptr 用堆上的引用计数表达共享所有权,灵活但有计数和线程同步开销;
-
weak_ptr 不拥有资源,用来观察
shared_ptr,专门解决循环引用和悬空访问; -
定制删除器 让智能指针可以管理
new[]、FILE*等非delete资源; -
make_shared 在多数场景下更高效,但要注意与
weak_ptr搭配时的生命周期问题。