
C++ 智能指针完整详解
前言
在 C++ 裸指针开发中,内存泄漏、野指针、重复释放、异常安全等问题长期困扰开发者。C++11 引入智能指针(Smart Pointer),基于 RAII(资源获取即初始化)思想自动管理堆内存生命周期,程序退出作用域时自动释放内存,彻底规避手动 new/delete 带来的各类内存隐患。
标准库智能指针全部定义于头文件 <memory>,核心三类:std::unique_ptr、std::shared_ptr、std::weak_ptr。
本文从原理、API、场景、坑点、性能对比完整拆解。
一、核心基础:RAII 机制
智能指针本质是栈上封装裸指针的模板类,核心逻辑 RAII:
对比裸指针痛点:
// 裸指针问题:异常会导致内存泄漏
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)创建方式
#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)移动所有权
unique_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)常用成员函数
auto 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. 使用场景
4. 自定义删除器
unique_ptr 删除器作为模板参数,支持自定义释放逻辑(管理文件句柄、socket 等):
// 自定义释放函数
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)
// make_shared:一次性分配对象+控制块,内存更高效
auto s1 = make_shared<int>(200);
shared_ptr<int> s2 = s1; // 拷贝,计数+1
cout << s1.use_count() << endl; // 2,查看当前引用数
(2)核心接口
auto 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,引用计数永远无法归零,内存永久泄漏。
struct 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. 使用场景
四、std::weak_ptr 弱智能指针(配套 shared_ptr)
1. 核心特性
- 不增加引用计数,仅作为 shared_ptr 的观察者;
- 解决 shared_ptr 循环引用泄漏;
- 不能直接解引用访问对象,必须通过 lock() 升级为 shared_ptr;
- 可检测对象是否已销毁(expired())。
2. 完整解决循环引用示例
#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. 常用接口
weak_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) |
| 内存开销 | 极小(仅裸指针) | 较高(控制块堆分配) | 极小(存储控制块地址) |
| 线程安全 | 无计数,移动需手动同步 | 计数增减原子安全,对象不安全 | 无计数,线程安全判断失效 |
| 循环引用问题 | 不存在(无法循环持有) | 会内存泄漏 | 专门解决循环引用 |
| 适用场景 | 绝大多数独占资源场景 | 多模块共享资源 | 配套 shared_ptr、打破循环 |
| C++ 标准支持 | C++11(make_unique C++14) | C++11 |
六、最佳实践规范(博客重点总结)
- 异常安全:内存分配失败不会造成裸指针泄漏;
- 内存连续分配,减少堆碎片;
七、常见踩坑汇总
坑 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[] ptr
- shared_ptr<T>:析构调用 delete ptr
删除器:用户自定义的可调用对象,接管资源释放逻辑,替换默认 delete
一、unique_ptr 删除器核心规则
1. 语法规则
unique_ptr 删除器是模板类型参数,类型固定在编译期,结构:
template< class T, class Deleter = default_delete<T> > class unique_ptr;
- 第二个模板参数为删除器类型,默认 std::default_delete<T>(封装 delete);
- 必须在定义时显式指定删除器类型;
- 无运行时开销,删除器对象存储在 unique_ptr 内部,不额外堆分配。
2. 四种删除器写法
(1)函数指针删除器
template<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推导类型:
auto del = [](Date* p){ delete[] p; };
unique_ptr<Date, decltype(del)> up(new Date[5], del);
(3)有捕获 lambda
不能转函数指针,只能decltype推导:
int logFlag = 1;
auto del = [logFlag](Date* p){
if(logFlag) cout << "释放数组" << endl;
delete[] p;
};
unique_ptr<Date, decltype(del)> up(new Date[5], del);
(4)仿函数( 结构体重载 ( ) )
struct 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 模板只有一个类型参数,删除器不写入模板:
template< class T > class shared_ptr;
- 删除器存放在堆上的控制块中,运行时存储;
- 任意可调用对象均可传入,声明无需修改模板;
- 拷贝、赋值不受删除器类型影响,不同删除器的shared_ptr<T>可互相赋值;
- 代价:控制块堆分配,轻微性能损耗。
2. 四种删除器通用写法
// 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 栈内,无堆开销 | 堆控制块,额外内存开销 |
| 类型兼容性 | 删除器不同则类型不同,无法转移 | 删除器不同仍为同类型,可赋值共享 |
| 写法复杂度 | 必须显式声明 Deleter 类型 | 直接传可调用对象,极简 |
| 适用场景 | 独占资源、追求极致性能 | 共享资源、外部句柄管理 |




