C++ 智能指针完整详解
前言
**在 C++ 裸指针开发中,内存泄漏、野指针、重复释放、异常安全等问题长期困扰开发者。C++11 引入智能指针(Smart Pointer),基于****RAII(资源获取即初始化)**思想自动管理堆内存生命周期,程序退出作用域时自动释放内存,彻底规避手动
new/delete带来的各类内存隐患。标准库智能指针全部定义于头文件**
<memory>****,核心三类:std::unique_ptr、std::shared_ptr、std::weak_ptr。**本文从原理、API、场景、坑点、性能对比完整拆解。
一、核心基础:RAII 机制
智能指针本质是栈上封装裸指针的模板类,核心逻辑 RAII:
- 构造函数:接管堆内存(
new出来的对象);- 析构函数:自动调用
delete释放内存;- 离开作用域(函数返回、代码块结束、异常抛出)时,栈对象自动销毁,析构触发内存释放。
对比裸指针痛点:
cpp// 裸指针问题:异常会导致内存泄漏 void bad_func() { int* p = new int(10); throw runtime_error("error"); // 抛出异常,delete 永远不会执行,内存泄漏 delete p; } // unique_ptr 自动兜底,异常也会释放内存 void good_func() { unique_ptr<int> p(new int(10)); throw runtime_error("error"); // 栈对象销毁,自动释放内存 }
二、std::unique_ptr 独占智能指针(推荐优先使用)
1. 核心特性
独占所有权:同一时刻只能有一个
unique_ptr持有对象,禁止拷贝,仅支持移动语义。
- 开销极低:底层仅封装一个裸指针,无额外引用计数;
- 性能几乎等同于裸指针,日常开发首选;
- 不支持拷贝构造 / 赋值,仅支持
std::move转移所有权。2. 基础用法
(1)创建方式
cpp#include <memory> #include <iostream> using namespace std; // 方式1:new 初始化(不推荐) unique_ptr<int> p1(new int(100)); // 方式2:make_unique(C++14 推荐,异常安全、简洁) auto p2 = make_unique<string>("hello unique"); // 管理数组 auto arr = make_unique<int[]>(5); // 长度5的int数组 arr[0] = 1;(2)移动所有权
cppunique_ptr<int> a = make_unique<int>(10); // unique_ptr<int> b = a; // 编译报错,禁止拷贝 unique_ptr<int> b = move(a); // 转移所有权 cout << (a == nullptr) << endl; // true,a 已置空 cout << *b << endl; // 10(3)常用成员函数
cppauto p = make_unique<int>(666); p.get(); // 返回内部裸指针 int* p.release(); // 释放所有权,返回裸指针,unique_ptr 置空(需手动delete) p.reset(); // 销毁当前管理对象,置空 p.reset(new int(999)); // 释放旧内存,接管新内存 if (p) {} // 重载bool,判断是否持有有效资源 *p; p->func(); // 重载解引用、箭头,和裸指针用法一致3. 使用场景
- 函数局部临时堆对象(默认首选);
- 容器存储独占资源(
vector<unique_ptr<T>>);- 类成员独占资源,无需共享;
- 工厂函数返回动态对象(移动语义自动优化,无拷贝开销)。
4. 自定义删除器
unique_ptr删除器作为模板参数,支持自定义释放逻辑(管理文件句柄、socket 等):
cpp// 自定义释放函数 void file_del(FILE* f) { cout << "关闭文件" << endl; fclose(f); } // 指定删除器类型 unique_ptr<FILE, decltype(file_del)*> fp(fopen("test.txt", "r"), file_del);数组版本默认调用
delete[],无需手动处理数组释放。
三、std::shared_ptr 共享智能指针
1. 核心特性
共享所有权:多份
shared_ptr可同时持有同一个对象,内部维护原子引用计数器。
- 引用计数规则:
- 创建
shared_ptr计数 = 1;- 拷贝 / 赋值给新 shared_ptr,计数 + 1;
- 销毁 shared_ptr(离开作用域、reset),计数 - 1;
- 计数归 0 时,自动释放堆内存。
- 原子计数器:多线程安全增减计数,但对象本身访问不保证线程安全;
- 额外开销:堆上分配控制块(计数 + 删除器),性能弱于 unique_ptr。
2. 基础用法
(1)创建(优先 make_shared)
cpp// make_shared:一次性分配对象+控制块,内存更高效 auto s1 = make_shared<int>(200); shared_ptr<int> s2 = s1; // 拷贝,计数+1 cout << s1.use_count() << endl; // 2,查看当前引用数(2)核心接口
cppauto sp = make_shared<string>("shared test"); sp.get(); // 获取裸指针 sp.reset(); // 计数-1,计数0则释放 sp.reset(new string()); // 接管新对象 sp.use_count(); // 返回当前引用计数 sp.unique(); // 判断计数是否等于1(C++17弃用)3. 经典坑:循环引用(内存泄漏根源)
当两个对象互相持有对方
shared_ptr,引用计数永远无法归零,内存永久泄漏。
cppstruct Node { int val; shared_ptr<Node> next; Node(int v) : val(v) {} }; void cycle_leak() { auto a = make_shared<Node>(1); auto b = make_shared<Node>(2); a->next = b; b->next = a; // 函数结束,a、b销毁,计数各减1,但仍为1,内存泄漏! }解决方案:搭配
std::weak_ptr打破循环引用。4. 使用场景
- 多个模块需要共享同一个资源;
- 缓存、全局资源池;
- 订阅回调、多持有者对象;
- 注意:无共享需求时,一律用 unique_ptr。
四、std::weak_ptr 弱智能指针(配套 shared_ptr)
1. 核心特性
- 不增加引用计数,仅作为
shared_ptr的观察者;- 解决 shared_ptr 循环引用泄漏;
- 不能直接解引用访问对象,必须通过
lock()升级为shared_ptr;- 可检测对象是否已销毁(
expired())。2. 完整解决循环引用示例
cpp#include <memory> struct Node { int val; weak_ptr<Node> next; // 改用 weak_ptr 打破循环 Node(int v) : val(v) {} }; void fix_cycle() { auto a = make_shared<Node>(1); auto b = make_shared<Node>(2); a->next = b; b->next = a; // 函数退出,计数归零,内存正常释放 } // weak_ptr 访问对象 auto wp = a->next; if (!wp.expired()) { // 对象未销毁 auto sp = wp.lock(); // 升级为shared_ptr,计数临时+1 cout << sp->val << endl; }3. 常用接口
cppweak_ptr<T> wp(sp); // 由shared_ptr构造 wp.expired(); // true=对象已释放 wp.lock(); // 返回shared_ptr,失效返回空shared_ptr wp.reset(); // 清空弱指针 wp.use_count(); // 获取关联shared_ptr的引用数4. 使用场景
- 打破 shared_ptr 循环引用;
- 缓存弱引用(不阻止资源释放,资源销毁后自动失效);
- 观察者模式,观察者不持有对象生命周期。
五、三种智能指针完整对比表
特性 unique_ptr shared_ptr weak_ptr 所有权模型 独占,不可拷贝 共享,可拷贝 无所有权,仅观察 引用计数 无 有(原子) 无 拷贝 / 赋值 禁止,仅移动 允许 允许(基于 shared_ptr) 内存开销 极小(仅裸指针) 较高(控制块堆分配) 极小(存储控制块地址) 线程安全 无计数,移动需手动同步 计数增减原子安全,对象不安全 无计数,线程安全判断失效 循环引用问题 不存在(无法循环持有) 会内存泄漏 专门解决循环引用 适用场景 绝大多数独占资源场景 多模块共享资源 配套 shared_ptr、打破循环 C++ 标准支持 C++11(make_unique C++14) C++11
六、最佳实践规范(博客重点总结)
- 优先使用 unique_ptr,无共享需求绝不使用 shared_ptr,性能最优;
- 创建智能指针统一用
make_unique/make_shared,避免裸 new:
- 异常安全:内存分配失败不会造成裸指针泄漏;
- 内存连续分配,减少堆碎片;
- 尽量避免
get()裸指针长期保存,防止悬垂;- 函数返回动态对象直接返回
unique_ptr,移动语义零开销;- 存在双向 / 环形依赖时,一方使用 weak_ptr 打破循环;
- 多线程场景:shared_ptr 计数安全,但对象读写必须加锁;
- 不要混用裸指针与智能指针管理同一资源,双重释放崩溃;
- 数组资源优先
make_unique<T[]>,自动调用delete[]。
七、常见踩坑汇总
坑 1:裸指针初始化多个 shared_ptr
int* p = new int(10); shared_ptr<int> s1(p); shared_ptr<int> s2(p); // 两个独立控制块,析构时重复delete,程序崩溃解决:全程只用 make_shared,不分离裸指针。
坑 2:函数传参裸指针给智能指针
void func(shared_ptr<int> sp) {} int* p = new int(10); func(p); // 隐式构造shared_ptr,离开函数释放,外部p变成野指针解决:传参直接传 shared_ptr,禁止裸指针隐式转换。
坑 3:weak_ptr 不判断 expired 直接 lock
对象已销毁时 lock 返回空 shared_ptr,解引用直接崩溃,必须先判断。
坑 4:unique_ptr 拷贝赋值
编译直接报错,改用
std::move转移所有权。
八、自定义删除器完整示例
智能指针默认释放逻辑:
unique_ptr<T>:析构调用delete ptr;数组特化unique_ptr<T[]>自动delete[] ptrshared_ptr<T>:析构调用delete ptr删除器:用户自定义的可调用对象,接管资源释放逻辑,替换默认
delete一、unique_ptr 删除器核心规则
1. 语法规则
unique_ptr删除器是模板类型参数,类型固定在编译期,结构:
cpptemplate< class T, class Deleter = default_delete<T> > class unique_ptr;
- 第二个模板参数为删除器类型,默认
std::default_delete<T>(封装 delete);- 必须在定义时显式指定删除器类型;
- 无运行时开销,删除器对象存储在
unique_ptr内部,不额外堆分配。2. 四种删除器写法
(1)函数指针删除器
cpptemplate<typename T> void ArrayDel(T* p) { cout << "数组释放 delete[]" << endl; delete[] p; } // Deleter类型:void(*)(Date*) 无捕获函数指针 unique_ptr<Date, void(*)(Date*)> up(new Date[5], ArrayDel<Date>);(2)无捕获 lambda
无捕获 lambda 可隐式转为函数指针,也可用
decltype推导类型:
cppauto del = [](Date* p){ delete[] p; }; unique_ptr<Date, decltype(del)> up(new Date[5], del);(3)有捕获 lambda
不能转函数指针,只能
decltype推导:
cppint logFlag = 1; auto del = [logFlag](Date* p){ if(logFlag) cout << "释放数组" << endl; delete[] p; }; unique_ptr<Date, decltype(del)> up(new Date[5], del);(4)仿函数( 结构体重载 ( ) )
cppstruct FileDel { void operator()(FILE* f) const { if(f) fclose(f); } }; // 仿函数作删除器 unique_ptr<FILE, FileDel> fp(fopen("a.txt","r"));3. 短板
删除器属于类型一部分,同 T、不同删除器的
unique_ptr是完全不同类型,无法互相赋值 / 移动。二、shared_ptr 删除器核心规则
1. 语法规则
shared_ptr模板只有一个类型参数,删除器不写入模板:
cpptemplate< class T > class shared_ptr;
- 删除器存放在堆上的控制块中,运行时存储;
- 任意可调用对象均可传入,声明无需修改模板;
- 拷贝、赋值不受删除器类型影响,不同删除器的
shared_ptr<T>可互相赋值;- 代价:控制块堆分配,轻微性能损耗。
2. 四种删除器通用写法
cpp// 1. 函数指针 shared_ptr<Date> sp1(new Date[5], ArrayDel<Date>); // 2. 内联lambda(最常用) shared_ptr<Date> sp2(new Date[5], [](Date* p){delete[] p;}); // 3. 仿函数 shared_ptr<FILE> sp3(fopen("a.txt","r"), FileDel{}); // 4. 带捕获lambda int flag = 1; shared_ptr<Date> sp4(new Date[5], [flag](Date* p){ if(flag) cout << "释放数组" << endl; delete[] p; });关键特性
拷贝
shared_ptr时,会同步复制控制块里的删除器;所有共享对象销毁时,统一调用同一个删除器。三、unique_ptr vs shared_ptr 删除器对比
维度 unique_ptr 删除器 shared_ptr 删除器 是否模板参数 是,影响类型 否,不影响类型 存储位置 unique_ptr 栈内,无堆开销 堆控制块,额外内存开销 类型兼容性 删除器不同则类型不同,无法转移 删除器不同仍为同类型,可赋值共享 写法复杂度 必须显式声明 Deleter 类型 直接传可调用对象,极简 适用场景 独占资源、追求极致性能 共享资源、外部句柄管理

