【C++智能指针】
在 C++ 编程中,动态内存管理是开发者绕不开的难题。手动使用new分配内存与delete释放内存,极易出现内存泄漏、重复释放、野指针等问题。为解决这些痛点,C++ 标准库引入了智能指针 —— 一种遵循 “资源获取即初始化(RAII)” 思想的类模板,能自动管理动态内存生命周期,大幅提升代码安全性与可靠性。本文将系统讲解智能指针的核心原理、三类标准智能指针()的用法,以及实际开发中的关键避坑点。
🌹 作者:云小逸
🤟 个人主页: 云小逸的主页
🤟 motto: 要敢于一个人默默的面对自己,强大自己才是核心。不要等到什么都没有了,才下定决心去做。种一颗树,最好的时间是十年前,其次就是现在!学会自己和解,与过去和解,努力爱自己。希望春天来之前,我们一起面朝大海,春暖花开!
🥇 专栏:
文章目录
-
- 📚 前言
- 一、智能指针的基础:为什么需要它?
- 二、标准库智能指针详解
- 三、实际开发避坑指南
- 四、总结:智能指针的选择与使用原则
- 📣 结语
📚 前言
在 C++ 编程中,动态内存管理是开发者绕不开的难题。手动使用new分配内存与delete释放内存,极易出现内存泄漏、重复释放、野指针等问题。为解决这些痛点,C++ 标准库引入了智能指针 —— 一种遵循 “资源获取即初始化(RAII)” 思想的类模板,能自动管理动态内存生命周期,大幅提升代码安全性与可靠性。本文将系统讲解智能指针的核心原理、三类标准智能指针(std::shared_ptr/std::unique_ptr/std::weak_ptr)的用法,以及实际开发中的关键避坑点。
一、智能指针的基础:为什么需要它?
1. 裸指针的痛点
普通指针(裸指针)的手动管理存在三大风险:
- 内存泄漏:忘记调用
delete,导致动态内存无法回收,长期占用系统资源; - 重复释放:对同一内存地址多次调用
delete,触发未定义行为(程序崩溃); - 野指针访问:内存释放后仍使用指针,访问无效内存区域,导致程序异常。
2. 智能指针的核心价值
智能指针通过 “将资源生命周期与对象生命周期绑定”,实现自动内存管理:
- 当智能指针对象离开作用域(如函数执行结束、对象被销毁)时,其析构函数会自动调用
delete,释放所管理的动态内存; - 开发者无需关注内存释放时机,只需专注业务逻辑,从根源上减少内存管理错误。
3. 简易智能指针:自定义Auto_ptr
为理解智能指针的底层逻辑,实现简化版Auto_ptr(含核心要素):
template // 模板类:支持管理任意类型指针
class Auto_ptr
{
public:
// 构造函数:接收裸指针,用explicit防止隐式类型转换
explicit Auto_ptr(Ty* ptr) : _ptr(ptr) {}
// 析构函数:自动释放内存(智能指针的核心)
~Auto_ptr()
{
if (_ptr != nullptr) // 避免空指针重复释放
{
delete _ptr; // 释放管理的动态内存
_ptr = nullptr; // 避免野指针
}
}
private:
Ty* _ptr; // 存储管理的裸指针
};
使用与原理:void testAutoPtr() {
// 创建Auto_ptr,管理new分配的动态内存(int类型,值为20)
Auto_ptr age(new int(20));
// 函数结束时,age离开作用域,析构函数自动调用delete释放内存
}
- 模板设计:
template<typename Ty>支持任意类型指针,通用性强; explicit关键字:防止意外隐式类型转换;- 注意:
Auto_ptr为简化版,不支持拷贝/赋值,实际需用标准库智能指针。
二、标准库智能指针详解
1. std::unique_ptr:独占所有权的轻量之选
(1)核心特性:如何实现独占性?
通过删除拷贝构造与赋值运算符禁止拷贝,仅支持移动语义转移所有权:template
class unique_ptr {
public:
// 显式删除拷贝构造函数:不允许用一个unique_ptr初始化另一个
unique_ptr(const unique_ptr&) = delete;
// 显式删除拷贝赋值运算符:不允许将一个unique_ptr赋值给另一个
unique_ptr& operator=(const unique_ptr&) = delete;
// 移动构造与移动赋值(支持所有权转移,C++11默认生成)
unique_ptr(unique_ptr&& other) noexcept;
unique_ptr& operator=(unique_ptr&& other) noexcept;
// ... 其他成员
};
(2)基本用法#include // 包含unique_ptr与make_unique
void testUniquePtr() {
// 1. 创建unique_ptr:推荐用make_unique(C++14及以后)
auto up1 = std::make_unique(10); // 管理int类型,值为10
std::unique_ptrstd::string up2(new std::string(“hello”)); // 直接用new(不推荐)
// 2. 访问对象:支持解引用(*)、成员访问(->)
std::cout << *up1 << std::endl; // 输出:10
std::cout << up2->size() << std::endl;// 输出:5(string长度)
// 3. 转移所有权:用std::move(),原指针变为空
auto up3 = std::move(up1); // up3接管内存,up1变为nullptr
if (up1 == nullptr) {
std::cout << "up1 is null" << std::endl; // 输出:up1 is null
}
// 4. 主动释放/重置:reset()
up2.reset(); // 释放"hello"内存,up2变为nullptr
up3.reset(new int(20)); // 释放原内存,重新管理新内存(值为20)
// 5. 释放所有权(不推荐):release()返回裸指针,需手动释放
int* rawPtr = up3.release();
delete rawPtr; // 必须手动delete,否则内存泄漏
}
(3)适用场景
- 资源仅需一个所有者(如函数内局部动态内存、单个对象管理的资源);
- 作为函数返回值(自动触发移动语义,无需手动
std::move); - 存储在容器中(需通过
std::move转移所有权,如vector.push_back(std::move(up)))。
2. std::shared_ptr:共享所有权的协作之选
(1)核心原理:引用计数
shared_ptr内部维护两个关键指针:
- 数据指针:指向管理的动态内存;
- 控制块指针:指向堆上 “控制块”,包含:
- 引用计数(
use_count):记录共享该内存的shared_ptr数量; - 弱引用计数(
weak_count):记录weak_ptr数量; - 自定义删除器、分配器等。
- 引用计数(
引用计数工作流程:
- 创建
shared_ptr时,引用计数初始化为 1; - 拷贝
shared_ptr(如sp2 = sp1)时,引用计数 + 1; shared_ptr销毁(离开作用域)或reset()时,引用计数 - 1;- 引用计数变为 0 时,释放管理的动态内存与控制块。
(2)基本用法#include
void testSharedPtr() {
// 1. 创建shared_ptr:推荐用make_shared(高效安全)
auto sp1 = std::make_shared(20); // 管理int,值为20,引用计数=1
std::shared_ptr sp2(new int(30)); // 直接用new(不推荐,效率低)
// 2. 共享所有权:拷贝操作,引用计数+1
auto sp3 = sp1; // sp1与sp3共享内存,引用计数=2
std::cout << sp1.use_count() << std::endl; // 输出:2(查看引用计数)
// 3. 访问对象:支持*、->
std::cout << *sp3 << std::endl; // 输出:20
// 4. 重置指针:sp1.reset()后,引用计数-1(变为1)
sp1.reset();
if (sp1 == nullptr) {
std::cout << "sp1 is null" << std::endl; // 输出:sp1 is null
}
std::cout << sp3.use_count() << std::endl; // 输出:1(仅sp3共享)
// 5. 自定义删除器(如管理数组)
// shared_ptr默认用delete释放,管理数组需指定delete[]
auto sp4 = std::shared_ptr<int>(new int[5], [](int* p) {
delete[] p; // 数组释放必须用delete[]
});
}
(3)make_shared的优势
- 效率更高:一次性分配 “数据内存” 与 “控制块内存”,减少 1 次内存分配;
- 更安全:避免异常导致内存泄漏,例如:
// 危险:若someFunction()抛出异常,new int(10)的内存未被shared_ptr接管 func(std::shared_ptr<int>(new int(10)), someFunction()); // 安全:make_shared先分配内存并绑定到shared_ptr,无泄漏风险 func(std::make_shared<int>(10), someFunction());
(4)适用场景
- 资源需多对象共享(如多个模块访问同一数据库连接、图像资源);
- 无法确定哪个对象最后释放资源(如多线程场景下的共享数据);
- 资源创建成本高(如网络连接、大文件句柄),共享可减少重复创建开销。
3. std::weak_ptr:解决循环引用的辅助之选
(1)循环引用:shared_ptr的致命隐患
当两个shared_ptr互相引用时,引用计数无法归零,导致内存泄漏:class B; // 前置声明
class A {
public:
std::shared_ptr b_ptr; // A持有B的shared_ptr
~A() { std::cout << “A destroyed” << std::endl; }
};
class B {
public:
std::shared_ptr a_ptr; // B持有A的shared_ptr
~B() { std::cout << “B destroyed” << std::endl; }
};
void testCycle() {
auto a = std::make_shared();
auto b = std::make_shared();
a->b_ptr = b; // A引用B,B的引用计数=2
b->a_ptr = a; // B引用A,A的引用计数=2
// 函数结束时,a、b销毁,引用计数均变为1(而非0)
// A、B的内存无法释放,导致内存泄漏(析构函数不会执行)
}
(2)weak_ptr的解决方案
将其中一方的shared_ptr改为weak_ptr(不增加引用计数),打破循环:class A {
public:
std::weak_ptr b_ptr; // 改为weak_ptr,不增加引用计数
~A() { std::cout << “A destroyed” << std::endl; }
};
class B {
public:
std::shared_ptr a_ptr;
~B() { std::cout << “B destroyed” << std::endl; }
};
void testWeakPtr() {
auto a = std::make_shared();
auto b = std::make_shared();
a->b_ptr = b; // weak_ptr不增加B的引用计数(仍为1)
b->a_ptr = a; // A的引用计数=2
// 函数结束时:
// 1. b销毁,A的引用计数=1;
// 2. a销毁,A的引用计数=0,A释放,B的引用计数=0,B释放;
// 3. 析构函数正常执行,无内存泄漏
}
(3)weak_ptr的基本用法
weak_ptr无法直接访问对象,需通过lock()转换为shared_ptr:void testWeakPtrBasic() {
auto sp = std::make_shared(22);
std::weak_ptr wp = sp; // weak_ptr绑定到shared_ptr,不增加引用计数
// 1. 检查对象是否存在:expired()
if (!wp.expired()) {
// 2. 转换为shared_ptr:lock()(成功则引用计数+1)
auto sp2 = wp.lock();
if (sp2) { // 必须判断sp2是否有效(避免线程安全问题)
std::cout << *sp2 << std::endl; // 输出:22
}
}
// 3. 查看引用计数:use_count()
std::cout << wp.use_count() << std::endl; // 输出:1(仅sp共享)
// 4. 释放观测:reset()
wp.reset(); // wp不再观测任何内存
}
4. 三类智能指针的共有操作
| 操作 | 描述 |
|---|---|
*p |
解引用指针,获取管理的对象(如*sp获取int值) |
p->mem |
成员访问,等价于(*p).mem(如sp->size()调用string方法) |
p.get() |
返回内部管理的裸指针(谨慎使用,避免与智能指针混用) |
p.reset() |
释放管理的内存,将指针置为nullptr |
p.reset(q) |
释放原内存,重新管理裸指针q指向的内存 |
p.swap(q) |
交换p与q管理的内存 |
p == nullptr |
判断指针是否为空(如if (sp == nullptr)) |
三、实际开发避坑指南
1. shared_ptr的线程安全:计数安全≠数据安全
- 误区:认为 “
shared_ptr是线程安全的,多线程操作无需加锁”; - 真相:
shared_ptr的引用计数操作是线程安全的(原子操作),但管理的数据不是; - 风险场景:
auto sp = std::make_shared<int>(0); // 线程1:修改数据 void thread1() { for (int i = 0; i < 10000; i++) { (*sp)++; // 数据竞争!多个线程同时修改同一int } } // 线程2:修改数据(与线程1竞争) void thread2() { for (int i = 0; i < 10000; i++) { (*sp)++; // 未定义行为,结果可能错误或程序崩溃 } } - 解决方案:对数据访问加锁(如
std::mutex),而非依赖shared_ptr的计数安全:std::mutex mtx; void threadSafeFunc() { std::lock_guard<std::mutex> lock(mtx); // 加锁,确保线程安全 (*sp)++; // 安全修改共享数据 }
2. unique_ptr自定义删除器:注意大小与类型
unique_ptr支持自定义删除器(如释放数组、文件句柄),但需关注两点:
(1)删除器状态影响unique_ptr大小
-
无状态删除器:如普通函数、无捕获
lambda(不捕获外部变量),unique_ptr大小与普通指针一致(64 位系统 8 字节):// 无捕获lambda作为删除器(无状态) auto delNoState = [](FILE* fp) { fclose(fp); }; std::unique_ptr<FILE, decltype(delNoState)> upFile( fopen("test.txt", "r"), delNoState ); // 64位系统中,upFile大小为8字节(仅存FILE*指针) -
有状态删除器:如捕获外部变量的
lambda、带成员的结构体删除器,unique_ptr需额外存储删除器状态,大小增加:// 捕获外部变量的lambda(有状态) int logLevel = 1; auto delWithState = [logLevel](int* p) { if (logLevel > 0) { std::cout << "Deleting int: " << *p << std::endl; } delete p; }; std::unique_ptr<int, decltype(delWithState)> upWithState( new int(100), delWithState ); // 64位系统中,upWithState大小为16字节(8字节int* + 4字节logLevel + 4字节对齐)
(2)不同删除器的unique_ptr是不同类型
即使删除器功能相同,类型不同的unique_ptr无法直接赋值:// 两个功能相同但类型不同的删除器
auto del1 = [](int* p) { delete p; };
auto del2 = [](int* p) { delete p; };
std::unique_ptr<int, decltype(del1)> up1(new int(10), del1);
std::unique_ptr<int, decltype(del2)> up2(new int(20), del2);
// 错误:up1与up2类型不同,无法直接赋值
// up1 = up2;
// 正确:通过std::move转移,且删除器类型需匹配
up1 = std::unique_ptr<int, decltype(del1)>(new int(30), del1);
3. 严禁智能指针管理栈内存:必触发双重释放
智能指针仅能管理动态内存(new/new[]分配),若误将栈内存地址传入,会导致 “双重释放”—— 栈内存会在函数结束时自动释放,智能指针析构时又会调用delete,触发程序崩溃。
风险示例(致命错误):void testStackMemory() {
int stackVar = 5; // 栈内存:函数结束后自动销毁
// 错误:用智能指针管理栈内存
std::shared_ptr spBad(&stackVar);
// 函数结束时:
// 1. stackVar随栈帧销毁;
// 2. spBad析构,调用delete &stackVar,释放已销毁的栈内存 → 程序崩溃
}
核心原则:
智能指针的构造参数,必须是new/new[]分配的动态内存地址,或通过make_shared/make_unique自动分配的内存(本质仍是动态内存)。
4. weak_ptr访问对象:以lock()返回值为准,而非expired()
weak_ptr需通过lock()转换为shared_ptr才能访问对象,但易陷入 “先判断expired()再lock()” 的误区 —— 即使expired()返回false(对象存在),在expired()与lock()之间,对象仍可能被其他线程释放,导致lock()返回空指针。
错误用法(线程不安全):void accessWeakPtrWrong(std::weak_ptr wp) {
if (!wp.expired()) { // 步骤1:判断对象存在
// 风险窗口:其他线程可能在此处释放对象
std::shared_ptr sp = wp.lock(); // 步骤2:转换为shared_ptr
std::cout << *sp << std::endl; // 若sp为空,解引用崩溃
}
}
正确用法(线程安全):
直接调用lock(),通过返回的shared_ptr是否非空判断对象是否有效 ——lock()内部会原子性地检查对象状态,避免线程安全问题:void accessWeakPtrRight(std::weak_ptr wp) {
// 直接lock(),原子性获取对象状态
std::shared_ptr sp = wp.lock();
if (sp) { // 以sp是否非空为准,安全判断
std::cout << *sp << std::endl; // 安全访问
} else {
std::cout << “Object has been destroyed” << std::endl;
}
}
5. 智能指针作为函数参数:避免不必要的拷贝与所有权转移
根据 “是否转移所有权” 选择传递方式,避免性能损耗或意外丢失所有权:
(1)shared_ptr:优先传const引用,避免拷贝
按值传递shared_ptr会触发拷贝构造,导致引用计数 + 1;函数结束后引用计数 - 1,增加不必要的原子操作开销。若仅需访问对象(不转移所有权),传const std::shared_ptr<T>&更高效:// 低效:按值传递,触发引用计数增减
void funcValue(std::shared_ptr sp) {
std::cout << *sp << std::endl;
}
// 高效:传const引用,无拷贝开销
void funcRef(const std::shared_ptr& sp) {
std::cout << *sp << std::endl;
}
// 调用示例
auto sp = std::make_shared(100);
funcValue(sp); // 引用计数先+1(进入函数)后-1(离开函数)
funcRef(sp); // 无引用计数操作,性能更优
(2)unique_ptr:按值传递需std::move,仅传引用不转移所有权
unique_ptr不支持拷贝,若需将所有权转移给函数,需通过std::move按值传递;若仅需临时访问对象(不转移所有权),传非const引用(std::unique_ptr<T>&):// 转移所有权:按值传递,需std::move
void takeOwnership(std::unique_ptr up) {
std::cout << *up << std::endl;
// up离开函数时自动释放内存
}
// 临时访问:传非const引用,不转移所有权
void accessWithoutOwnership(std::unique_ptr& up) {
*up = 200; // 可修改对象内容,不影响所有权
}
// 调用示例
auto up = std::make_unique(100);
accessWithoutOwnership(up); // 不转移所有权,up仍有效
takeOwnership(std::move(up)); // 转移所有权,up变为nullptr
// 此时up已为空,不可再访问
6. shared_ptr管理数组:必须自定义删除器
std::shared_ptr默认使用delete释放资源,而new[]分配的数组需用delete[]释放(匹配内存分配方式)。若直接用shared_ptr管理数组,会因 “释放方式不匹配” 触发未定义行为(内存泄漏或崩溃)。
错误用法:// 错误:shared_ptr默认用delete释放,不匹配new[]
std::shared_ptr spArr(new int[5]);
// 析构时调用delete spArr.get(),而非delete[] → 内存泄漏(数组元素未正确释放)
正确用法:
通过自定义删除器指定delete[],或使用std::default_delete<T[]>(标准库提供的数组删除器):// 方式1:lambda作为删除器,显式调用delete[]
std::shared_ptr spArr1(
new int[5], [](int* p) { delete[] p; }
);
// 方式2:使用std::default_delete<T[]>(更简洁)
std::shared_ptr spArr2(
new int[5], std::default_delete<int[]>()
);
// 方式3:C++20及以后,用std::make_shared_for_overwrite(需配合删除器)
#ifdef __cpp_lib_make_shared_for_overwrite
auto spArr3 = std::shared_ptr(
std::make_shared_for_overwrite<int[]>(5),
std::default_delete<int[]>()
);
#endif
注意:
std::unique_ptr有数组特化版本(std::unique_ptr<T[]>),默认用delete[]释放,无需自定义删除器,因此管理数组优先选择unique_ptr。
四、总结:智能指针的选择与使用原则
1. 智能指针选择优先级
-
优先用
std::unique_ptr:
资源仅需单个所有者、无共享需求时,unique_ptr轻量(无引用计数开销)、高效,且能强制独占性,避免意外共享。 -
次选用
std::shared_ptr:
资源需多对象共享、无法确定释放顺序时,使用shared_ptr,但需注意避免循环引用(配合weak_ptr解决)。 -
辅助用
std::weak_ptr:
不单独使用,仅作为shared_ptr的补充 —— 解决循环引用、观测对象状态(不影响生命周期)。
2. 核心使用原则
- 拒绝裸指针混用:不将智能指针管理的裸指针(
get()返回值)暴露给外部代码,避免双重释放或野指针访问; - 优先用工厂函数创建:
make_shared/make_unique比直接new更高效、安全,减少内存分配次数与泄漏风险; - 明确所有权转移:
unique_ptr转移所有权需显式用std::move,避免意外丢失所有权;shared_ptr拷贝时清楚引用计数变化; - 多线程操作加锁:
shared_ptr的引用计数安全不代表数据安全,多线程修改共享数据需额外加锁(如std::mutex)。
通过掌握智能指针的原理、用法与避坑点,开发者可彻底摆脱手动内存管理的困扰,写出更安全、高效、易维护的 C++ 代码。智能指针不仅是内存管理工具,更是 C++“资源安全” 思想的核心体现 —— 让资源的生命周期与对象绑定,从根源上减少人为错误。
📣 结语
感谢你耐心看完,恭喜你比昨天的你进步了一点点哦
如果你觉得我写的不错,记得给我点赞,收藏 和 关注哦(。・ω・。)
让我们一起加油,向美好的未来奔去。让我们从一无所知的新手逐渐成为专家。为自己点赞吧!
更多推荐



所有评论(0)