
前面我们聊了 C++ 异常的抛出、捕获、栈展开,也聊了异常安全的痛点 ------ 一旦异常抛出,try 块中未释放的内存、锁、文件句柄就可能泄漏,哪怕手动 catch 后再释放,也容易因逻辑疏漏留下隐患。
而智能指针,正是解决这类问题的「神器」。它利用 RAII 思想,把资源的生命周期和对象绑定 ,哪怕中途抛出异常,栈展开时也会自动调用析构函数释放资源,让你彻底告别手动管理资源的烦恼。接下来,我们就聊聊智能指针是如何和异常安全完美配合的。

1.智能指针应用的场景
下面程序中我们可以看到,new 了以后,我们也 delete 了,但是因为抛异常导致后面的
delete 没有得到执行 ,所以就内存泄漏了,所以我们需要 new 以后捕获异常,捕获到异常后
delete 内存,再把异常抛出,但是因为 new 本身也可能抛异常,连续的两个 new 和下面的
Divide 都可能会抛异常,让我们处理起来很麻烦。智能指针放到这样的场景里面就让问题简
单多了。
cpp
#include<iostream>
using namespace std;
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没有得到释放。
// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。
// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案
// 是智能指针,否则代码太戳了
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;
}
2.RAII和智能指针的设计思路
1.什么是RAII
RAII = Resource Acquisition Is Initialization
资源获取即初始化
它是 C++ 独有的、解决资源管理 + 异常安全的最核心设计思想。
一句话总结:
把资源交给一个对象管理,利用对象生命周期自动释放资源。
RAII 的核心规则构造函数 :创建对象时,自动获取资源(new 内存、打开文件、加锁)
析构函数:对象销毁时,自动释放资源(delete、关闭文件、解锁)不管程序是正常结束,还是因为异常中途退出,栈上对象都会被自动销毁 → 析构函数一定会执行 → 资源一定会释放。
RAII 的本质,就是利用 C++ 对象生命周期结束时,会自动调用析构函数这一特性,把资源的释放交给编译器自动完成。
2.智能指针的设计思路
智能指针类除了满足 RAII 的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类一样,**重载 operator* / operator-> / operator **等运算符,方便访问资源。
cpp
template<class T>
class SmartPtr
{
public:
// RAII
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;
};
3.C++标准库中智能指针的使用
#include<memory>
1.auto_ptr (已被弃用)
auto_ptr 是 C++98 里最早的智能指针,也是 C++ 第一个官方 RAII 实现,用来自动管理堆内存,防止异常泄漏。
但C++11 已经弃用,C++17 直接移除,现在绝对不推荐使用!
因此只做了解
核心特点
•最早的智能指针,基于 RAII,自动 delete 内存;
•拥有权转移式管理指针:
•一个 auto_ptr 赋值给另一个时,原对象会被置空,变成 "无效指针";
•没有引用计数,同一时间只有一个对象拥有内存。
但为什么不推荐呢?
致命缺陷
•赋值 / 拷贝后,原对象悬空,极易引发空指针崩溃;
•不能用于容器(如 vector<auto_ptr>),容器拷贝会直接炸;
•不支持数组,不能用 delete\[\];
•行为不符合直觉,坑多、难维护。

2.unique_ptr
unique_ptr 是 独占所有权 的智能指针,遵循 RAII 原则,同一时间只能有一个 unique_ptr 指向同一块内存,它离开作用域时,会自动释放内存。
核心特点:
•独占所有权 :不支持拷贝,只支持移动
•轻量高效 :和裸指针占用相同内存,无额外开销
•异常安全:无论正常退出还是异常抛出,内存都会自动释放
1.创建与使用
cpp
#include <iostream>
#include <memory>
using namespace std;
int main() {
// 方式1:直接构造
unique_ptr<int> p1(new int(10));
// 方式2:推荐使用 make_unique(C++14 起)
auto p2 = make_unique<int>(20);
// 像裸指针一样使用
cout << *p1 << endl;
cout << *p2 << endl;
// 自动释放,无需 delete
return 0;
}
2. 移动(移动语义)
cpp
unique_ptr<int> p1 = make_unique<int>(10);
// unique_ptr<int> p2 = p1; // 编译报错!禁止拷贝
unique_ptr<int> p2 = move(p1); // 所有权转移给 p2
// 此时 p1 变为空,p2 拥有内存
if (!p1) {
cout << "p1 已经不再拥有内存" << endl;
}

为什么它比 auto_ptr 安全?
•auto_ptr 是**"拷贝即掏空",行为隐蔽且危险**;而 unique_ptr:
•拷贝直接编译报错,从根源上杜绝悬空指针
•只有 std::move 才会转移所有权,行为明确
•支持数组版本:unique_ptr<int\[\]>,自动用**delete\[\]**释放
3.shared_ptr
shared_ptr 是一种共享所有权 的智能指针,它通过引用计数机制来管理内存:
每多一个 shared_ptr 指向同一块内存,引用计数就 +1;每销毁一个 shared_ptr,引用计数就 -1;当计数降到 0 时,内存会被自动释放。
1. 创建与初始化
cpp
#include <iostream>
#include <memory>
using namespace std;
int main() {
// 方式1:直接构造(不推荐,存在异常安全问题)
shared_ptr<int> p1(new int(10));
// 方式2:推荐使用 make_shared(更高效、异常安全)
auto p2 = make_shared<int>(20);
cout << *p1 << endl;
cout << *p2 << endl;
return 0;
}
2. 引用计数的变化
cpp
auto p1 = make_shared<int>(10);
cout << p1.use_count() << endl; // 输出 1,只有 p1 指向它
auto p2 = p1; // 拷贝构造,共享所有权
cout << p1.use_count() << endl; // 输出 2
cout << p2.use_count() << endl; // 输出 2
p1.reset(); // p1 放弃所有权
cout << p2.use_count() << endl; // 输出 1


shared_ptr 是基于引用计数的共享所有权智能指针,解决了多个对象共享同一块内存的问题,同时保证了异常安全。但使用时必须注意循环引用的陷阱,需要配合 weak_ptr 来避免内存泄漏。
4.weak_ptr(辅助指针)
weak_ptr 是一种弱引用智能指针,它只能从 shared_ptr 或其他 weak_ptr 构造,本身不拥有对象的所有权
•不会改变 shared_ptr 的引用计数
•不能直接访问对象,必须先通过 lock() 升级为 shared_ptr
•可以检测对象是否已经被释放
•核心作用 :打破 shared_ptr 的循环引用,避免内存泄漏。
cpp
#include <iostream>
#include <memory>
using namespace std;
int main() {
auto sp = make_shared<int>(10);
weak_ptr<int> wp = sp; // 弱引用,不增加计数
cout << "sp 引用计数:" << sp.use_count() << endl; // 输出 1
// 尝试升级为 shared_ptr
if (auto p = wp.lock()) {
cout << "对象存在,值为:" << *p << endl;
cout << "升级后计数:" << p.use_count() << endl; // 输出 2
} else {
cout << "对象已被释放" << endl;
}
return 0;
}

5.shared_ptr和weak_ptr解决引用循环
•如下图所述场景,n1 和 n2 析构后,管理两个节点的引用计数减到 1
•右边的节点什么时候释放呢,左边节点中的_next 管着呢,_next 析构后,右边的节点就释放了。
_next 什么时候析构呢,_next 是左边节点的的成员,左边节点释放,_next 就析构了。
•左边节点什么时候释放呢,左边节点由右边节点中的_prev 管着呢,_prev 析构后,左边的节点就释放了。
•_prev 什么时候析构呢,_prev 是右边节点的成员,右边节点释放,_prev 就析构了。
•至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏
•把 ListNode 结构体中的_next 和_prev 改成 weak_ptr,weak_ptr 绑定到 shared_ptr 时不会增加它的引用计数,_next 和_prev 不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题。

cpp
#include <iostream>
#include <memory>
using namespace std;
struct Node {
shared_ptr<Node> next;
weak_ptr<Node> prev; // 这里改成 weak_ptr!
~Node() {
cout << "Node 析构!内存已释放" << endl;
}
};
int main() {
auto n1 = make_shared<Node>();
auto n2 = make_shared<Node>();
n1->next = n2;
n2->prev = n1;
// 正常释放!
return 0;
}
4.删除器
所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为 new 经常使用,所以为了简洁一点,unique_ptr 和 shared_ptr 都特化了一份 的版本。
cpp
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
class Fclose
{
public:
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
// 仿函数对象做删除器
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
// 函数指针做删除器
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
// lambda表达式做删除器
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
// 实现其他资源管理的删除器
shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
5.C++11和boost中智能指针的关系
一句话总结:C++11 的标准智能指针,基本就是把 Boost.SmartPtr 成熟实现 "抄进标准库",并做了少量简化与扩展。
•Boost 库是为 C++ 语言标准库提供扩展的一些 C++ 程序库的总称,Boost 社区建立的初衷之一就是为 C++ 的标准化工作提供可供参考的实现,Boost 社区的发起人 Dawes 本人就是 C++ 标准委员会的成员之一。在 Boost 库的开发中,Boost 社区也在这个方向上取得了丰硕的成果,C++11 及之后的新语法和库有很多都是从 Boost 中来的。
•C++ 98 中产生了第一个智能指针 auto_ptr。
•C++ boost 给出了更实用的 scoped_ptr/scoped_array 和 shared_ptr/shared_array 和 weak_ptr 等。
•C++ TR1,引入了 shared_ptr 等,不过注意的是 TR1 并不是标准版。
•C++ 11,引入了 unique_ptr 和 shared_ptr 和 weak_ptr。需要注意的是 unique_ptr 对应 boost 的 scoped_ptr。并且这些智能指针的实现原理是参考 boost 中的实现的。
6.shared_ptr简单实现
cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<functional>
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;
}
};
namespace wyy
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T*ptr=nullptr)
:_ptr(ptr)
,_count(new int(1))
{ }
~shared_ptr()
{
/*delete _ptr;
delete _count;*/
release();
}
template<class D>
shared_ptr(T*ptr,D del)
:_ptr(ptr)
,_count(new int(1))
,_del(del)
{ }
shared_ptr(const shared_ptr<T>& ptr)
:_ptr(ptr)
,_count(ptr._count)
,_del(ptr._del)
{
(*_count)++;
}
shared_ptr<T>& operator=(const shared_ptr<T>& ptr)
{
if (_ptr != ptr._ptr)
{
release();
_ptr = ptr._ptr;
_count = ptr._count;
_del = ptr._del;
(*_count)++;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count()
{
return *_count;
}
T* get()
{
return _ptr;
}
private:
void release()
{
if (--(*_count) == 0)
{
_del(_ptr);
delete _count;
_ptr = nullptr;
_count = nullptr;
}
}
T* _ptr;
int* _count;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
}