C++ 智能指针完整详解
一、智能指针的核心基础
1. 核心作用
智能指针是C++标准库(STL) 提供的模板类,本质是对原生裸指针(raw pointer)的封装,核心解决2个致命问题:
- 手动管理动态内存时,忘记调用
delete/delete[]导致的内存泄漏; - 程序异常退出时,
delete语句执行不到导致的内存泄漏; - 规避原生指针的野指针、二次释放等问题。
2. 实现原理
所有智能指针的底层都基于 C++ 经典的 RAII 编程思想:
RAII(资源获取即初始化):在构造函数中申请/持有资源(这里是动态内存),在析构函数中释放资源。智能指针是栈上的对象,当栈对象生命周期结束(出作用域、异常退出),析构函数一定会被调用,内存就一定会被释放,无需手动管理。
3. 头文件
所有C++标准智能指针,都定义在 <memory> 头文件中,使用前必须包含:
#include<memory>二、C++ 所有标准智能指针(按重要性排序)
✅ 1. std::unique_ptr 「独占型智能指针」—— 最常用、优先级最高
核心特性
unique_ptr 是独占所有权的智能指针:一块动态内存,永远只能被一个 unique_ptr 指向,不允许拷贝、不允许赋值。
- 它是 C++11 新增的,轻量级(无额外内存开销,和原生指针效率一致);
- 支持指向单个对象,也原生支持指向数组(
unique_ptr<int[]>); - 支持指定自定义删除器,用于释放特殊资源(比如文件句柄、网络句柄)。
关键规则
- ❌ 禁止拷贝:
unique_ptr<int> p2 = p1;编译报错; - ❌ 禁止赋值:
p2 = p1;编译报错; - ✅ 允许移动:通过
std::move()转移内存的所有权,转移后原指针变为空指针,新指针持有内存,这是唯一合法的“传递”方式。
常用示例
#include<memory>#include<iostream>usingnamespace std;intmain(){// 方式1:创建指向单个对象的unique_ptr(推荐make_unique,C++14支持) unique_ptr<int> p1 =make_unique<int>(10); cout <<*p1 << endl;// 输出:10// 方式2:创建指向数组的unique_ptr(原生支持,无需额外处理) unique_ptr<int[]> p2 =make_unique<int[]>(5);for(int i =0; i <5;++i) p2[i]= i;// 数组正常赋值// 方式3:移动语义转移所有权 unique_ptr<int> p3 =move(p1); cout <<*p3 << endl;// 输出:10if(p1 ==nullptr){ cout <<"p1已为空"<< endl;// 输出:p1已为空}// 手动释放内存(可选,析构时会自动释放,reset等价于置空+释放) p3.reset();return0;}使用场景
✅ 优先使用 unique_ptr,这是C++官方推荐的首选智能指针,满足90%的场景:
- 所有「独占内存」的场景(一个对象只被一个指针管理);
- 函数的返回值(返回动态对象,所有权转移给调用方);
- 类的成员变量(持有类的私有资源,避免拷贝);
- 容器存储(比如
vector<unique_ptr<int>>,利用移动语义存储)。
✅ 2. std::shared_ptr 「共享型智能指针」—— 共享所有权场景必备
核心特性
shared_ptr 是共享所有权的智能指针:一块动态内存,可以被多个 shared_ptr 共同指向,所有指针共享同一块内存的所有权。
- 它是 C++11 新增的,底层通过引用计数(reference count) 实现;
- 支持指向单个对象,C++17开始原生支持数组(
shared_ptr<int[]>); - 支持自定义删除器,支持拷贝、赋值,使用灵活。
核心原理:引用计数
shared_ptr 内部维护一个引用计数器,记录当前有多少个 shared_ptr 指向同一块内存:
- 当新的
shared_ptr指向该内存(拷贝、赋值),计数器 +1; - 当某个
shared_ptr生命周期结束(出作用域、reset),计数器 -1; - 当计数器变为 0 时(没有任何指针指向这块内存了),自动调用
delete释放内存。
关键规则
- ✅ 允许拷贝:
shared_ptr<int> p2 = p1;合法,计数器+1; - ✅ 允许赋值:
p2 = p1;合法,p2原内存的计数器-1,p1内存的计数器+1; - ✅ 可以通过
use_count()查看当前引用计数; - ✅ 可以通过
unique()判断是否是唯一持有者(计数器=1)。
常用示例
#include<memory>#include<iostream>usingnamespace std;intmain(){// 方式1:创建shared_ptr(推荐make_shared,C++11支持,效率更高) shared_ptr<int> p1 =make_shared<int>(20); cout <<*p1 << endl;// 输出:20 cout << p1.use_count()<< endl;// 输出:1(只有p1指向内存)// 拷贝:计数器+1 shared_ptr<int> p2 = p1; cout << p2.use_count()<< endl;// 输出:2(p1、p2都指向内存)// 赋值:计数器变化 shared_ptr<int> p3; p3 = p2; cout << p3.use_count()<< endl;// 输出:3// 手动减少引用计数 p1.reset(); cout << p3.use_count()<< endl;// 输出:2(p1释放,计数器-1)return0;// 函数结束,p2、p3生命周期结束,计数器减到0,内存释放}使用场景
✅ 适用于多个地方需要共享同一个对象的场景:
- 多个函数、多个对象、多个容器需要访问同一块内存;
- 容器存储(
vector<shared_ptr<int>>),支持自由拷贝和遍历; - 类之间的关联关系(比如多个子对象指向同一个父对象)。
⚠️ 致命缺陷:循环引用(内存泄漏)
这是 shared_ptr唯一的坑,也是高频面试题:
循环引用:两个(或多个)shared_ptr 互相指向对方,导致它们的引用计数器永远无法减到0,最终内存泄漏。典型场景:双向链表的节点、父子对象互相持有对方的 shared_ptr。
// 循环引用示例:内存泄漏!structNode{int val; shared_ptr<Node> next;// 节点指向另一个节点};intmain(){ shared_ptr<Node> n1 =make_shared<Node>(); shared_ptr<Node> n2 =make_shared<Node>(); n1->next = n2;// n1持有n2 n2->next = n1;// n2持有n1// 函数结束时,n1和n2的计数器都是2,减到1后不再变化,内存永远不释放!return0;}这个问题的唯一解决方案,就是下面的 std::weak_ptr。
✅ 3. std::weak_ptr 「弱引用型智能指针」—— 仅用于解决shared_ptr的循环引用
核心定位
weak_ptr 是 C++11 新增的,辅助型智能指针,不能独立使用,必须配合 shared_ptr 一起使用!它不是一个真正的“智能指针”,而是 shared_ptr 的“补充工具”。
核心特性
weak_ptr 是弱引用:可以指向 shared_ptr 管理的内存,但不会增加引用计数,也不拥有内存的所有权。
- ❌ 没有重载
operator*和operator->,不能直接解引用访问内存; - ❌ 不能单独创建,只能通过
shared_ptr赋值/拷贝得到; - ✅ 不会导致循环引用,因为它不增加引用计数;
- ✅ 可以检测自己指向的内存是否还存在(判活),避免访问野指针。
核心作用
✅ 唯一用途:解决 std::shared_ptr 的循环引用问题,没有其他场景需要使用 weak_ptr。
核心使用方法
- 通过
shared_ptr构造weak_ptr:weak_ptr<int> wp = sp;(sp是shared_ptr); - 要访问内存时,必须通过
wp.lock()方法升级为 shared_ptr:- 如果内存还存在,
lock()返回一个有效的shared_ptr,此时可以正常解引用; - 如果内存已释放,
lock()返回一个空的 shared_ptr,避免访问野指针;
- 如果内存还存在,
- 可以通过
wp.expired()判断内存是否已释放(等价于wp.lock() == nullptr)。
解决循环引用示例(关键修复)
structNode{int val; weak_ptr<Node> next;// 把shared_ptr改为weak_ptr,问题解决!};intmain(){ shared_ptr<Node> n1 =make_shared<Node>(); shared_ptr<Node> n2 =make_shared<Node>(); n1->next = n2;// weak_ptr指向n2,引用计数不增加 n2->next = n1;// weak_ptr指向n1,引用计数不增加// 函数结束时,n1和n2的计数器减到0,内存正常释放!return0;}❌ 4. std::auto_ptr 「废弃的智能指针」—— 绝对不要使用!
基本背景
auto_ptr 是 C++98 标准中引入的第一个智能指针,也是C++历史上的过渡产物。
核心问题(为什么被废弃)
auto_ptr 表面上是“独占型”智能指针,但它的拷贝/赋值语义存在严重缺陷:
- 它允许拷贝和赋值,但是拷贝/赋值后,原指针会被置为空指针,新指针持有内存;
- 这种“隐式转移所有权”的行为,会导致代码中出现不可预知的空指针访问,极易引发程序崩溃,是C++中的“坑中之坑”。
废弃&移除标准
- C++11:标记为废弃,引入
unique_ptr替代它,unique_ptr从语法上禁止拷贝,彻底规避了这个问题; - C++17:完全移除,标准库中不再提供
std::auto_ptr。
结论
✅ 记住:永远不要在任何C++代码中使用 std::auto_ptr,无论是新项目还是老项目,都要替换为 std::unique_ptr。三、补充:智能指针的创建方式(推荐写法)
所有智能指针都有两种创建方式,强烈推荐第一种:
方式1:使用 make_xxx 函数(推荐,优先级最高)
std::make_unique<T>(args):创建unique_ptr,C++14 支持;std::make_shared<T>(args):创建shared_ptr,C++11 支持。
优点:
- 代码更简洁,无需手动写
new; - 内存分配更高效(
make_shared会一次性分配对象内存和引用计数内存); - 避免内存泄漏(如果构造函数抛出异常,
make_xxx会自动释放已申请的内存); - 避免裸指针暴露,更安全。
方式2:通过裸指针构造(不推荐,应急使用)
// 应急写法,不推荐 unique_ptr<int>p1(newint(10)); shared_ptr<int>p2(newint(20));缺点:如果构造过程中抛出异常,可能导致内存泄漏,且代码可读性差。
四、核心总结(必记,面试高频)
1. 智能指针核心价值
用 RAII 思想自动管理动态内存,彻底解决手动 new/delete 导致的内存泄漏问题。
2. 所有标准智能指针速览
| 智能指针 | C++标准 | 核心特性 | 能否独立使用 | 核心用途 | 优先级 |
|---|---|---|---|---|---|
| std::unique_ptr | C++11 | 独占所有权、轻量级、无拷贝 | ✅ 能 | 90%的场景,优先使用 | ⭐⭐⭐⭐⭐ |
| std::shared_ptr | C++11 | 共享所有权、引用计数、可拷贝 | ✅ 能 | 多指针共享内存的场景 | ⭐⭐⭐⭐ |
| std::weak_ptr | C++11 | 弱引用、不增计数、依赖shared | ❌ 不能 | 解决shared_ptr的循环引用 | ⭐⭐⭐ |
| std::auto_ptr | C++98 | 独占所有权、拷贝有坑 | ✅ 能 | 已废弃,绝对不用 | ❌ |
3. 选型优先级(黄金法则)
能用 unique_ptr,就不用 shared_ptr;weak_ptr 只在解决循环引用时用
- 优先选择
std::unique_ptr:轻量、高效、无坑,覆盖绝大多数场景; - 只有需要共享内存所有权时,才选择
std::shared_ptr; - 只有在
std::shared_ptr出现循环引用时,才用std::weak_ptr修复; - 彻底忘掉
std::auto_ptr。
✨ 面试高频考点(补充)
- 智能指针的实现原理?—— RAII 思想,构造持有资源,析构释放资源。
- unique_ptr 和 shared_ptr 的区别?—— 独占vs共享,有无引用计数,效率差异。
- shared_ptr 的坑是什么?怎么解决?—— 循环引用,用 weak_ptr 解决。
- weak_ptr 为什么能解决循环引用?—— 弱引用不增加引用计数。
- auto_ptr 为什么被废弃?—— 拷贝赋值会导致原指针为空,易崩溃。