代码仓库shanchuann/CPP-Learninng
C++11标准依托右值引用、移动语义两大核心语法特性,搭建起一套完备、安全且高效的智能指针体系,彻底解决了传统裸指针在内存管理中的内存泄漏、重复释放、悬空指针等核心痛点,覆盖独占管理、共享管理、循环引用防护等全场景开发需求,是现代C++工程开发中内存管理的标准方案。实际使用时需包含<memory>头文件,整套体系包含三类智能指针,三者各司其职、相互配合,分别适配不同的资源管理逻辑,全面满足日常开发的各类内存管控需求。
unique_ptr独占式智能指针
unique_ptr是C++11推出的独占所有权型智能指针,专门替代C++98中存在严重设计缺陷的auto_ptr,也是日常开发中独占式资源管理的首选方案。其核心规则为同一时间仅允许一个指针持有资源所有权,安全性与运行效率远超传统裸指针与旧版auto_ptr,从根源规避了独占资源的非法共享问题。
特点
- 排他所有权模式:两个指针绝对不能指向同一个资源,从根源杜绝重复释放、悬空指针问题;
- 禁用拷贝语义:不提供拷贝构造函数与左值赋值重载,禁止值拷贝传递,阻断非法共享路径;
- 支持移动语义:提供移动构造与移动赋值函数,支持所有权在指针间安全转移,是其传参、返回机制;
- 适配多类型资源:内置删除器机制,可同时管理单个堆对象、动态数组,还能自定义删除器管理非堆内存资源;
- 容器安全:满足STL容器对元素的语义要求,存入容器后不会出现auto_ptr的所有权转移崩溃问题;
- 使用贴近裸指针:重载*、->运算符,日常调用方式和普通裸指针完全一致,上手成本极低。
源码实现
unique_ptr源码采用模板类架构,搭配单个对象与数组的特化默认删除器,完整实现独占语义、移动语义、自定义删除器核心机制,完全贴合标准库底层设计逻辑,兼顾底层原理学习与工程实战参考,具体简化仿写实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
| template<class _Ty> struct default_deleter { void operator()(_Ty* ptr) const { delete ptr; } };
template<class _Ty> struct default_deleter<_Ty[]> { void operator()(_Ty* ptr) const { delete[] ptr; } };
struct DelFile { void operator()(FILE* fp) const { if (fp != nullptr) { fclose(fp); } } };
template<class _Ty, class _Dx = default_deleter<_Ty>> class unique_ptr { public: using pointer = _Ty*; using element_type = _Ty; using deleter_type = _Dx; private: pointer mPtr; deleter_type mDeleter; public: explicit unique_ptr(pointer p = nullptr) : mPtr(p) {}
unique_ptr(const unique_ptr&) = delete; unique_ptr& operator=(const unique_ptr&) = delete;
~unique_ptr() { if (mPtr != nullptr) { reset(); } }
template<class _Ty2, class _Dx2> unique_ptr(unique_ptr<_Ty2, _Dx2>&& other) { reset(other.release()); }
template<class _Ty2, class _Dx2> unique_ptr& operator=(unique_ptr<_Ty2, _Dx2>&& other) { if (this != reinterpret_cast<unique_ptr<_Ty, _Dx>*>(&other)) { reset(other.release()); } return *this; }
pointer release() { pointer old = mPtr; mPtr = nullptr; return old; }
void reset(pointer ptr = nullptr) { if (mPtr != nullptr) { mDeleter(mPtr); } mPtr = ptr; }
void swap(unique_ptr& other) { std::swap(this->mPtr, other.mPtr); }
pointer get() const { return mPtr; }
_Dx& get_deleter() { return mDeleter; } const _Dx& get_deleter() const { return mDeleter; }
explicit operator bool() const { return mPtr != nullptr; }
_Ty& operator*() const { return *mPtr; } pointer operator->() const { return get(); } };
template<class _Ty, class _Dx> class unique_ptr<_Ty[], _Dx> { public: using pointer = _Ty*; using element_type = _Ty; using deleter_type = _Dx; private: pointer mPtr; deleter_type mDeleter; public: unique_ptr(pointer p = nullptr) : mPtr(p) {} unique_ptr(const unique_ptr&) = delete; unique_ptr& operator=(const unique_ptr&) = delete; ~unique_ptr() { if (mPtr != nullptr) reset(); }
template<class _Ty2, class _Dx2> unique_ptr(unique_ptr<_Ty2, _Dx2>&& other) { reset(other.release()); } unique_ptr& operator=(unique_ptr<_Ty2, _Dx2>&& other) { if (this != reinterpret_cast<unique_ptr<_Ty[], _Dx>*>(&other)) { reset(other.release()); } return *this; }
_Ty& operator[](std::size_t i) const { return get()[i]; }
pointer release() { pointer old = mPtr; mPtr = nullptr; return old; } void reset(pointer ptr = nullptr) { if(mPtr) mDeleter(mPtr); mPtr = ptr; } void swap(unique_ptr& other) { std::swap(mPtr, other.mPtr); } pointer get() const { return mPtr; } explicit operator bool() const { return mPtr != nullptr; } };
|
创建unique_ptr对象
创建unique_ptr需严格遵循独占所有权核心规则,严禁用同一裸指针复用初始化多个实例,具体规范用法与典型错误案例区分如下:
- 规范创建方式:直接new初始化、make_unique创建(C++14及以上)
// 标准单个对象创建 ``unique_ptr<Int> pInt(new Int(10)); ``// C++14推荐:make_unique,更安全无内存碎片 ``unique_ptr<Int> pInt2 = make_unique<Int>(20);
- 规范禁忌:同一裸指针初始化多个unique_ptr,会导致重复释放引发程序崩溃
Int* ip = new Int(10); ``unique_ptr<Int> pInta(ip); // 错误 ``unique_ptr<Int> pIntb(ip); // 错误用法,双指针管理同一对象
辅助函数
unique_ptr的辅助函数功能专一、逻辑清晰,是资源管理的常用工具,日常开发高频使用,无模糊歧义,核心功能与使用边界明确如下:
- get():返回内部裸指针,不转移所有权,仅用于访问,禁止手动delete该指针;
- reset():释放当前托管资源,可传入新指针重新绑定,无参数则置空;
- release():释放所有权,断开指针关联,返回原裸指针,不释放资源,资源释放责任转交给调用方;
- swap():交换两个unique_ptr的托管对象,无拷贝、无释放,效率极高;
- operator bool():隐式转换为布尔值,快速判断指针是否持有有效对象。
1 2 3 4 5 6
| unique_ptr<Int> pInta(new Int(10)); Int* p = pInta.release(); pInta.reset(new Int(20)); unique_ptr<Int> pIntb(new Int(30)); pInta.swap(pIntb);
|
禁止拷贝构造与左值赋值
禁用拷贝构造与左值赋值,是unique_ptr独占语义的核心保障。源码中通过=delete关键字显式禁用这两种操作,任何尝试拷贝的行为都会直接触发编译报错,从语法层面彻底杜绝所有权混淆、资源重复管理的问题。
1 2 3 4 5 6 7 8
| unique_ptr<Int> pInta(new Int(10)); unique_ptr<Int> pIntb(pInta); unique_ptr<Int> pIntc; pIntc = pInta;
void func(unique_ptr<Int> pInt) {} func(pInta);
|
支持移动构造与移动赋值
unique_ptr虽不支持常规拷贝,但完美兼容移动语义,通过std::move将左值转为右值引用,即可实现所有权的安全转移。转移完成后原指针会自动置空,不会出现资源重复托管的问题,这也是unique_ptr作为函数返回值、跨作用域传递的核心机制。
1 2 3 4 5 6 7 8 9 10 11 12 13
| unique_ptr<Int> pInta(new Int(10));
unique_ptr<Int> pIntb(std::move(pInta));
unique_ptr<Int> pIntc; pIntc = std::move(pIntb);
unique_ptr<Int> func() { unique_ptr<Int> tmp(new Int(20)); return tmp; } unique_ptr<Int> pIntd = func();
|
管理动态数组
unique_ptr专门提供数组特化版本,针对动态数组做了专项优化,不仅重载了[]运算符方便元素访问,还搭配专属delete[]删除器,解决了auto_ptr无法管理数组的短板,使用时需明确指定数组类型模板参数。
1 2 3 4 5 6
| unique_ptr<Int[]> pArr(new Int[5]{1,2,3,4,5});
pArr[0] = 10; pArr[1] = 20;
|
make_unique函数
make_unique是C++14标准正式纳入的智能指针工厂函数,作为unique_ptr的专属创建工具,是现代C++工程中创建独占式智能指针的首选方案。相较于直接通过new初始化,它依托可变模板参数与完美转发特性,实现了更安全、更灵活、更高效的对象创建,从语法层面规避裸指针暴露、内存泄漏、异常不安全等常规问题,完全适配unique_ptr的独占语义与生命周期管控规则。
可变模板参数与完美转发原理
make_unique的核心设计依托可变模板参数与完美转发两大C++11特性,实现对任意构造函数的无损耗适配,无需修改源码即可适配各类对象创建场景:可变模板参数通过template<class T, class... Args>语法,接收任意数量、任意类型的构造参数,突破固定参数列表限制,适配类的默认构造、有参构造、多参构造、重载构造等全部场景,通用性极强;完美转发借助std::forward<Args>(args)实现参数无损转发,保留参数原本的左值/右值属性,既不产生额外拷贝,也不改变参数语义,保证构造效率与原生new完全一致,同时适配移动构造、拷贝构造等各类构造场景。
make_unique标准库源码实现
以下为C++14标准中make_unique的核心源码实现,分为单个对象版本与动态数组特化版本,逻辑简洁严谨,完全贴合unique_ptr底层设计,附带详细注释便于理解底层运行逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <memory>
template <class T, class... Args> inline unique_ptr<T> make_unique(Args&&... args) { return unique_ptr<T>(new T(std::forward<Args>(args)...)); }
template <class T> inline unique_ptr<T[]> make_unique(size_t size) { return unique_ptr<T[]>(new T[size]()); }
template <class T, class... Args> unique_ptr<T> make_unique(Args&&... args) = delete;
|
- 模板声明:
template<class T, class... Args>中,T代表要创建的对象类型,Args...为可变模板参数包,可接收0个、1个或多个任意类型参数,覆盖所有构造场景;
- 右值引用参数:
Args&&... args为通用引用,既能接收左值参数,也能接收右值参数,是实现完美转发的基础;
- 完美转发调用:
std::forward<Args>(args)...将参数包原样转发给T的构造函数,保留参数的值属性,杜绝不必要的拷贝,最大化提升构造效率;
- 数组版本限制:数组版本仅支持传入数组长度,不支持自定义初始化参数,同时禁用固定长度数组的创建,避免开发者误用导致内存越界;
- 返回值优化:函数直接返回unique_ptr临时对象,编译器会触发RVO返回值优化,省略拷贝/移动步骤,直接构造目标对象,运行效率远超手动new初始化。
使用特性与代码
make_unique的使用方式简洁直观,依托可变模板参数可适配各类构造场景,同时保留原有使用规范,示例如下:
1 2 3 4 5 6 7 8 9 10 11
| auto pEmpty = make_unique<Int>();
auto pInt = make_unique<Int>(10);
auto pStu = make_unique<Student>(1001, "张三", 20);
auto pArr = make_unique<Int[]>(5);
pArr[0] = 1; pArr[1] = 2;
|
注意事项
make_unique虽为最优创建方案,但存在明确使用边界:不支持自定义删除器,源码中仅封装默认删除器,无法适配文件句柄、网络套接字等非堆内存资源,这类场景必须改用new直接初始化+自定义删除器的方式;数组版本无初始化参数,无法在创建数组时指定元素初始值,仅能默认初始化;兼容C++11,C++11环境可直接复制上述源码手动实现make_unique,补齐标准缺失;异常安全,对象内存与unique_ptr控制块一次性分配,即便构造过程抛出异常,也不会产生内存泄漏,这是直接new无法比拟的优势。
非堆内存的资源管理
unique_ptr的应用价值并非局限于堆内存管理,其自定义删除器机制是突破内存范畴、适配各类系统原生资源的核心设计,完美践行C++的**RAII(资源获取即初始化)**核心思想。借助这一机制,可将文件句柄、网络套接字、线程互斥锁、进程句柄、设备描述符等非堆内存资源,交由unique_ptr实现自动化生命周期管控,彻底杜绝手动释放遗漏、重复释放、资源泄漏等问题,尤其适配C语言风格原生API的资源管理场景,是工程中跨语言、跨API资源管控的优质方案。
自定义删除器
unique_ptr允许在模板参数中指定自定义删除器类型,替代默认的delete/delete[]释放逻辑,删除器本质是可调用对象,包括函数对象、lambda表达式、函数指针三类形式,核心要求是重载operator(),接收unique_ptr托管的原生资源指针,在unique_ptr析构、reset时自动调用完成资源释放。设计自定义删除器需遵循三大核心原则:空指针防护,必须先判断资源指针非空再执行释放,避免空指针调用崩溃;精准释放,匹配对应资源的专属释放API,文件用fclose、套接字用closesocket,严禁混用释放逻辑;无异常抛出,释放逻辑内禁止抛出异常,防止程序意外崩溃。
FILE文件
FILE*是C标准库的文件操作句柄,属于典型的非堆内存资源,手动管理时极易因忘记调用fclose导致文件句柄泄漏,尤其在函数多分支返回、异常抛出场景下,手动释放逻辑极易被遗漏。通过unique_ptr搭配自定义删除器,可实现文件资源的全自动生命周期管控,优化后的完整代码包含空指针校验、文件打开失败处理、规范读写逻辑,完全适配工程实际使用场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| #include <cstdio> #include <memory> #include <cstring>
struct DelFile { void operator()(FILE* fp) const { if (fp != nullptr) { fclose(fp); fp = nullptr; } } };
unique_ptr<FILE, DelFile> open_file(const char* filename, const char* mode) { if (filename == nullptr || mode == nullptr) { return unique_ptr<FILE, DelFile>(nullptr); } FILE* fp = fopen(filename, mode); return unique_ptr<FILE, DelFile>(fp); }
void test_file_manage() { auto file_ptr = open_file("test.txt", "r"); if (!file_ptr) { printf("文件打开失败,请检查文件路径或权限!\n"); return; }
char read_buf[1024] = {0}; size_t read_len = fread(read_buf, sizeof(char), sizeof(read_buf) - 1, file_ptr.get()); if (read_len > 0) { printf("读取文件内容:%s\n", read_buf); }
}
void write_file_demo() { auto file_ptr = open_file("log.txt", "w+"); if (file_ptr) { const char* write_msg = "unique_ptr管理文件句柄示例"; fwrite(write_msg, sizeof(char), strlen(write_msg), file_ptr.get()); printf("文件写入成功!\n"); } }
|
其他非堆资源管理
自定义删除器的设计思路具备通用性,可直接迁移到所有系统原生资源管理场景,以下为两类高频场景的删除器实现,核心逻辑与文件删除器完全一致,仅更换专属资源释放API,可直接套用至工程开发中。
网络套接字资源
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include <winsock2.h> #pragma comment(lib, "ws2_32.lib")
struct DelSocket { void operator()(SOCKET* sock) const { if (sock != nullptr && *sock != INVALID_SOCKET) { closesocket(*sock); delete sock; } } };
|
线程互斥锁资源
1 2 3 4 5 6 7 8 9 10 11
| #include <windows.h>
struct DelMutex { void operator()(HANDLE* mutex) const { if (mutex != nullptr && *mutex != nullptr) { CloseHandle(*mutex); delete mutex; } } };
|
- 删除器类型必须匹配:unique_ptr的第二个模板参数是删除器类型,而非实例,需严格指定自定义删除器结构体/类名;
- 禁止混用默认删除器:非堆资源绝对不能使用默认删除器,默认delete会尝试释放堆内存,导致程序崩溃;
- 禁止手动释放资源:交由unique_ptr托管后,严禁手动调用fclose、closesocket等释放API,否则会导致重复释放;
- 与make_unique不兼容:make_unique仅支持默认删除器,管理非堆资源必须通过直接构造unique_ptr的方式,无法使用make_unique;
- 独占性不变:非堆资源同样遵循unique_ptr独占语义,禁止多个指针托管同一资源,避免重复释放。
unique_ptr与哈希容器
在C++标准库中,unique_ptr不建议直接作为hash_map、unordered_map等哈希关联容器的键,这是工程开发中常见的易错点。强行混用不仅会触发编译报错,还可能引发所有权悬空、程序崩溃、内存泄漏等运行时问题,属于unique_ptr使用场景中需要严格规避的重要禁忌。需要明确区分:unique_ptr可作为哈希容器的值正常存储,但不可作为键使用,二者底层逻辑与语义要求存在本质冲突。
底层不兼容
哈希容器中,C++11前为hash_map,C++11及以后标准为unordered_map,对键(Key)有三大硬性底层要求,而unique_ptr的独占所有权设计,从根源上无法满足任何一条,这也是二者无法兼容的核心原因:
- 无默认哈希函数特化:C++标准库并未为unique_ptr提供std::hash模板特化,编译器无法为unique_ptr类型生成合法的哈希值,而哈希容器的底层存储依赖哈希值映射,缺少哈希函数会直接导致编译失败;
- 禁用拷贝与等值比较:哈希容器键需要支持拷贝、赋值与等值比较操作,unique_ptr显式禁用拷贝构造与左值赋值,仅支持移动语义,无法完成键的拷贝与等值判断,不满足容器键的语义要求;
- 所有权唯一性导致键不稳定:unique_ptr的独占特性决定了指针无法被复制,仅能通过std::move转移所有权,转移后原指针立即悬空,无法作为稳定、持久的键使用,会直接破坏哈希容器的存储结构与查找逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include<unordered_map> #include<memory>
class Shape { public: virtual void draw() = 0; virtual ~Shape() = default; }; class Circle : public Shape { public: void draw() override {} };
int main() { std::unordered_map<size_t, std::unique_ptr<Shape>> shape_map; std::unique_ptr<Circle> cir = std::make_unique<Circle>();
shape_map[std::hash<std::unique_ptr<Circle>>()(cir)] = std::move(cir);
return 0; }
|
上述代码会直接触发两类核心问题:一是编译期报错,提示std::hash无对应unique_ptr的特化版本,无法生成合法哈希值;二是逻辑运行错误,即便强行自定义哈希函数绕过编译报错,std::move转移所有权后原unique_ptr会立即悬空,容器键失去实际意义,后续查找完全失效,还会打乱对象所有权管理逻辑,引发内存异常。
替代方案
实际开发中如需在哈希容器中存储智能指针管理的对象,需遵循以下合规替代方案,兼顾内存安全与开发实用性,规避各类隐性风险。
- 方案一:用对象唯一标识作为键,unique_ptr作为值(首选):为对象分配固定唯一ID、字符串等可哈希类型作为键,unique_ptr仅作为容器值存储,既满足哈希容器要求,又保留unique_ptr的独占语义,是工程最优解;
- 方案二:改用shared_ptr作为键:标准库为shared_ptr提供了默认的std::hash特化,且支持拷贝与等值比较,可直接作为哈希容器键,适合资源共享场景;
- 方案三:裸指针作为临时键(谨慎使用):仅在对象生命周期完全可控、无悬空风险的场景下,用get()获取裸指针作为键,严禁手动释放裸指针,避免悬空;
- 方案四:自定义哈希函数(不推荐):强行为unique_ptr自定义哈希函数,会破坏其独占语义,极易引发所有权冲突,仅特殊场景临时使用。
很多开发者容易混淆“键”与“值”的使用边界,unique_ptr作为容器值存储完全合法,仅禁止作为键使用;此外,部分第三方库或自定义容器看似支持unique_ptr作为键,本质是破坏了独占所有权规则,后续极易出现内存异常,严格遵循标准库设计规范,才是规避风险的核心原则。
使用总结
unique_ptr作为C++独占式智能指针的标准实现,凭借独占所有权、自动释放、零额外性能开销三大核心优势,完美适配现代C++工程中各类独占资源的生命周期管控场景。结合日常开发高频需求,梳理出四大核心实用场景,覆盖类成员、临时资源、设计模式、特殊对象四大维度,每类场景都精准匹配unique_ptr核心特性,彻底规避裸指针的各类内存问题,具体场景解析与实战应用如下。
对象内部专属资源管理
该场景是unique_ptr最基础、最常用的工程用法,适配类内部私有专属资源的管控需求,这类资源仅归当前对象所有,不对外共享、不跨对象传递,完全贴合unique_ptr的独占语义。相较于传统裸指针作为类成员,unique_ptr托管的成员资源无需手动在析构函数中编写释放逻辑,依托RAII机制,在当前对象析构时,会自动触发unique_ptr的析构函数,完成资源释放,从根源杜绝因忘记写析构逻辑、析构逻辑遗漏导致的内存泄漏。同时,即便类对象在构造、成员函数执行过程中抛出异常,栈展开时也会正常调用unique_ptr析构,保障异常场景下的资源安全,无需额外编写异常捕获释放逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| class Computer { public: Computer() { printf("计算机资源初始化完成\n"); } ~Computer() { printf("计算机资源已释放,无内存泄漏\n"); } void Program() { printf("执行编程任务\n"); } };
class Student { private: unique_ptr<Computer> pc; public: Student() : pc(make_unique<Computer>()) {} Student(const Student&) = delete; Student& operator=(const Student&) = delete; Student(Student&&) noexcept = default; Student& operator=(Student&&) noexcept = default;
void study() { if (pc) { pc->Program(); } } };
void test_class_member() { Student stu; stu.study(); }
|
托管专属资源的类建议禁用拷贝,避免多个对象共享同一独占资源;优先通过make_unique初始化成员,杜绝裸指针暴露;移动语义可保留,支持对象转移,适配容器存储、函数返回等场景。
函数内部临时资源托管
函数内部临时申请的堆内存、临时资源,是裸指针最容易引发内存泄漏的场景,尤其函数存在多分支return、条件判断提前退出、业务逻辑抛出异常时,手动编写的delete逻辑极易被跳过,导致资源泄漏。unique_ptr完美解决这一痛点,将临时资源托管给栈上的unique_ptr对象,无论函数以何种方式退出(正常返回、提前return、异常抛出),栈上的unique_ptr都会在作用域结束时正常析构,自动释放托管资源,实现函数级别的自动化资源管控,全程无需手动干预,彻底消除临时资源泄漏风险。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| class MyConn { private: bool is_open; public: MyConn() : is_open(false) {} ~MyConn() { printf("数据库连接已关闭,资源释放完成\n"); } bool openConn(const char* addr) { is_open = (addr != nullptr); return is_open; } bool isOpen() const { return is_open; } void execQuery(const char* sql) { if (is_open) { printf("执行SQL:%s\n", sql); } } };
void func(const char* db_addr) { unique_ptr<MyConn> pConn(new MyConn()); bool conn_flag = pConn->openConn(db_addr);
if (!conn_flag) { printf("数据库连接失败,函数退出\n"); return; }
pConn->execQuery("select * from user");
if (db_addr == nullptr) { throw std::invalid_argument("数据库地址非法"); }
printf("业务执行完成,函数正常退出\n"); }
void test_func_temp() { try { func("127.0.0.1:3306"); func(nullptr); } catch (...) { } }
|
临时资源严禁用裸指针手动管理;unique_ptr声明在函数栈上,不要动态分配unique_ptr本身;资源使用完毕无需手动reset,依赖作用域自动析构即可,效率最优。
工厂方法模式返回值
工厂方法模式是面向对象中常用的创建型设计模式,作用是封装对象创建逻辑,实现接口与实现分离,传统工厂模式返回裸指针,极易出现调用方忘记释放、重复释放、资源归属不明确等问题。unique_ptr作为工厂方法的返回值,完美契合工厂模式的设计理念:一方面,通过unique_ptr明确传递资源所有权,工厂创建对象后,将所有权完全转移给调用方,工厂不再管控资源生命周期;另一方面,调用方接收unique_ptr后,无需关心资源释放,依托智能指针自动管理,同时独占语义避免了对象被非法共享,适配多态场景下的派生类对象返回,是现代C++工厂模式的标准返回方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #include <string>
class Shape { public: virtual void draw() const = 0; virtual ~Shape() = default; static unique_ptr<Shape> factory(const std::string& type); };
class Circle : public Shape { public: void draw() const override { printf("绘制圆形,半径:10\n"); } ~Circle() override { printf("圆形对象资源已释放\n"); } };
class Square : public Shape { public: void draw() const override { printf("绘制正方形,边长:8\n"); } ~Square() override { printf("正方形对象资源已释放\n"); } };
unique_ptr<Shape> Shape::factory(const std::string& type) { if (type == "Circle") { return make_unique<Circle>(); } if (type == "Square") { return make_unique<Square>(); } return nullptr; }
void test_factory() { auto circle = Shape::factory("Circle"); auto square = Shape::factory("Square");
if (circle) circle->draw(); if (square) square->draw();
}
|
工厂方法严禁返回裸指针,统一返回unique_ptr;利用编译器的RVO返回值优化,无额外移动开销;调用方通过std::move可转移所有权,适配长期存储场景;空指针返回需做有效性判断,避免空调用。
特殊构造函数对象的管理
工程中部分对象出于设计规范、安全性考虑,会禁用外部直接创建,常见场景包括构造函数私有(单例模式、工具类)、禁止外部new(受控对象)、仅允许静态方法创建等,这类对象无法通过常规new/delete手动管理生命周期,裸指针难以适配,极易出现资源泄漏或重复释放。unique_ptr可完美适配这类特殊对象,通过配合友元、静态创建函数,实现对象的受控创建,同时依托自定义删除器或默认删除器,完成自动化生命周期管控,尤其适配单例模式的改良版(非全局单例、受控单例),解决传统单例手动释放、内存泄漏的痛点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| class SpecialTool { private: SpecialTool() { printf("特殊工具类初始化\n"); } SpecialTool(const SpecialTool&) = delete; SpecialTool& operator=(const SpecialTool&) = delete;
friend unique_ptr<SpecialTool> createSpecialTool();
public: ~SpecialTool() { printf("特殊工具类资源释放完成\n"); } void doWork() { printf("执行特殊工具业务逻辑\n"); } };
unique_ptr<SpecialTool> createSpecialTool() { return unique_ptr<SpecialTool>(new SpecialTool()); }
class Singleton { private: Singleton() { printf("单例对象初始化\n"); } ~Singleton() { printf("单例对象释放完成\n"); } Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;
public: static unique_ptr<Singleton>& getInstance() { static unique_ptr<Singleton> instance(new Singleton()); return instance; } void run() { printf("单例业务执行\n"); } };
void test_special_obj() { auto tool = createSpecialTool(); if (tool) tool->doWork();
auto& single = Singleton::getInstance(); if (single) single->run(); }
|
特殊对象通过静态函数封装创建逻辑,对外仅暴露unique_ptr;私有构造类需通过友元或静态函数授权创建;单例场景用static unique_ptr托管,避免全局裸指针的释放问题;禁止外部手动释放,完全交由unique_ptr管控。
使用场景
综上四大场景,unique_ptr的使用原则始终围绕独占所有权、自动释放、无额外性能损耗展开,凡是无需共享、专属单一持有者的资源,均优先选用unique_ptr。它既兼顾了裸指针的高效性,又具备RAII的安全性,是现代C++开发中替代裸指针的首选方案,配合make_unique、移动语义、自定义删除器,可覆盖堆内存、非堆资源、特殊对象全品类管控,全面提升代码的稳定性与可维护性。
shared_ptr共享式智能指针
shared_ptr是C++11标准推出的共享所有权型智能指针,既是废弃auto_ptr的全面替代方案,也弥补了unique_ptr独占语义无法适配多对象共用资源的核心短板。它基于引用计数机制实现多指针共享同一堆对象,允许多个shared_ptr同时指向并协同管理同一份资源,依托RAII机制自动完成生命周期管控,无需手动释放内存,从根源解决共享场景下的内存泄漏、重复释放、悬空指针三大痛点,广泛适配容器存储、跨模块数据共享、多线程资源共用、多态对象管理等高频工程场景。
相较于unique_ptr的零额外性能开销,shared_ptr会占用极小量堆内存存储引用计数控制块,以此换取极致的共享安全性,是C++11智能指针体系中适配范围最广的共享型内存管理工具,也是STL容器存储智能指针的标准首选方案。
特性梳理
shared_ptr的所有特性均围绕共享所有权设计,兼顾安全性、灵活性与工程实用性,覆盖底层逻辑与日常使用全维度,核心特性梳理如下:
- 共享所有权模式:打破独占限制,同一份资源可被多个shared_ptr共同持有,所有权归属全体共享指针,而非单一指针,资源生命周期由全体持有者共同决定;
- 引用计数管控生命周期:内置全局共享的引用计数,通过计数动态增减精准判断资源释放时机,计数归零时自动调用删除器销毁对象,全程自动化无需手动干预;
- 支持拷贝与赋值:开放拷贝构造与左值赋值运算符,拷贝或赋值时仅递增引用计数,不复制资源本体,效率远高于资源拷贝;
- 兼容移动语义:支持移动构造与移动赋值,转移所有权时不修改引用计数,原指针自动置空,兼顾所有权转移灵活性与资源安全性;
- 支持自定义删除器:与unique_ptr逻辑一致,可绑定专属删除器,不仅能管理堆内存对象、动态数组,还能适配文件句柄、网络套接字等非堆内存系统资源;
- STL容器兼容:满足标准容器元素可拷贝、可赋值的硬性要求,可直接存入vector、list、map、unordered_map等所有STL容器,彻底规避auto_ptr存入容器的崩溃风险;
- 裸指针使用体验:重载*、->运算符,日常调用语法与普通裸指针完全一致,上手门槛低,无需额外学习特殊用法。
引用计数机制
引用计数是shared_ptr的底层核心机制,所有共享逻辑、生命周期管控均围绕该机制展开,也是其与unique_ptr的核心设计差异,原理与运行规则严谨清晰,是掌握shared_ptr的关键。
控制块结构
每个被shared_ptr管理的对象,都会配套一个独立堆内存分配的引用计数控制块,控制块内包含两大原子计数:shared_ptr强引用计数(_Uses)、weak_ptr弱引用计数(_Weaks)。该控制块由所有指向同一资源的shared_ptr全局共享,确保计数实时同步,且计数增减采用原子操作,保障多线程并发场景下的计数准确性,避免数据竞争问题。
动态变更规则
- 初始化计数:通过new对象构造第一个shared_ptr时,强引用计数初始化为1,代表当前有1个指针持有该资源;
- 拷贝/赋值递增:拷贝构造或左值赋值生成新shared_ptr时,对应资源的强引用计数自动+1;
- 析构/重置递减:shared_ptr析构、调用reset重置或接管新资源时,原资源强引用计数自动-1;
- 计数归零释放:强引用计数减至0时,代表无任何指针持有该资源,立即调用删除器释放对象内存,弱引用计数归零时销毁控制块;
- 移动语义不改动计数:移动构造或赋值仅做所有权转移,不修改引用计数,原指针置空,不影响资源持有者数量。
引用计数实操示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <iostream> #include <memory> using namespace std;
class Int { private: int value; public: Int(int x = 0) : value(x) { cout << "Create Int: " << value << endl; } ~Int() { cout << "Destroy Int: " << value << endl; } void PrintInt() const { cout << "Value: " << value << endl; } };
int main() { shared_ptr<Int> pInta(new Int(10)); cout << "pInta 引用计数:" << pInta.use_count() << endl;
shared_ptr<Int> pIntb(pInta); cout << "pInta/pIntb 引用计数:" << pInta.use_count() << endl;
shared_ptr<Int> pIntc; pIntc = pInta; cout << "当前引用计数:" << pInta.use_count() << endl;
return 0; }
|
标准创建规范
shared_ptr的创建分为安全首选方案与特殊场景备选方案,工程开发中需严格遵守规范,杜绝各类内存风险,具体创建方式与注意事项如下:
首选方案:std::make_shared(补充缓存局部性优势)
make_shared是标准库提供的专属工厂函数,可一次性分配对象内存与引用计数控制块,将对象数据与引用计数在堆上连续存储,这带来了两大关键优势:
减少内存分配次数与碎片:单次分配同时完成对象与控制块的内存申请,相比 new + shared_ptr 构造的两次分配,显著降低了内存碎片,提升了内存管理效率,同时异常安全性拉满,完全规避裸指针暴露风险。
提升缓存局部性(Cache Locality):由于对象内存与引用计数控制块在物理地址上相邻,CPU 访问时更易被同时加载到高速缓存(Cache)中,可有效减少缓存缺失(Cache Misses)次数。根据程序局部性原理(时间局部性与空间局部性),连续内存布局能让数据访问更贴合 CPU 缓存机制,当需要频繁访问对象或修改引用计数时,可将缓存缺失操作减少约一半,在性能敏感场景下能带来可观的速度提升。
1 2 3 4
| shared_ptr<Int> pInt = make_shared<Int>(20);
shared_ptr<Int> pEmpty = make_shared<Int>();
|
备选方案:裸指针直接构造
该方式仅适用于无法使用 make_shared 的特殊场景,比如对象构造函数私有、需要绑定自定义删除器等,使用时需牢记核心禁忌:严禁用同一裸指针初始化多个 shared_ptr,否则会生成多组独立控制块,最终引发重复释放崩溃。
1 2 3 4 5 6 7
| shared_ptr<Int> pInt(new Int(10));
Int* ip = new Int(10); shared_ptr<Int> p1(ip); shared_ptr<Int> p2(ip);
|
补充说明:
引入 Cache 的理论基础是程序局部性原理,包括时间局部性和空间局部性:
- 时间局部性:最近被 CPU 访问的数据,短期内 CPU 还会再次访问;
- 空间局部性:被 CPU 访问的数据附近的数据,短期内 CPU 也会访问。
因此,将刚访问过的数据缓存在 Cache 中,下次访问时可直接从 Cache 读取,速度能得到数量级提升。CPU 访问的数据在 Cache 中存在,称为“命中”(Hit),反之则称为“缺失”(Miss)。make_shared 的连续内存布局正是通过优化空间局部性,有效降低了 Cache Miss 概率。
shared_ptr底层源码
以下为还原标准库核心逻辑的shared_ptr仿写源码,包含原子引用计数、自定义删除器、数组特化、拷贝/移动/析构全功能实现,适配底层原理学习,注释详细易懂,贴合标准库设计思路:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
| #include <memory> #include <atomic> #include <iostream> using namespace std;
template <typename _Ty> struct My_Shared_Deleter { void operator()(_Ty* ptr) const { delete ptr; } };
template <typename _Ty> struct My_Shared_Deleter<_Ty[]> { void operator()(_Ty* ptr) const { delete[] ptr; } };
template <typename _Ty> class My_RefCount { private: _Ty* _Ptr; atomic<int> _Uses; atomic<int> _Weaks; public: My_RefCount(_Ty* ptr = nullptr) : _Ptr(ptr), _Uses(0), _Weaks(0) { if (_Ptr) _Uses = 1; } void Incref() { ++_Uses; } void Incwref() { ++_Weaks; } int Decref() { return --_Uses; } int Decwref() { return --_Weaks; } int use_count() const { return _Uses.load(); } _Ty* get() const { return _Ptr; } };
template <class _Ty, class Deleter = My_Shared_Deleter<_Ty>> class My_Shared_Ptr { public: using pointer = _Ty*; using element_type = _Ty; using deleter_type = Deleter; private: pointer _Ptr; My_RefCount<_Ty>* _RefBlock; deleter_type _Deleter; public: My_Shared_Ptr() : _Ptr(nullptr), _RefBlock(nullptr) {} explicit My_Shared_Ptr(pointer p) : _Ptr(p), _RefBlock(nullptr) { if (_Ptr) _RefBlock = new My_RefCount<_Ty>(_Ptr); } My_Shared_Ptr(const My_Shared_Ptr& other) { _Ptr = other._Ptr; _RefBlock = other._RefBlock; if (_RefBlock) _RefBlock->Incref(); } My_Shared_Ptr(My_Shared_Ptr&& other) noexcept { _Ptr = other._Ptr; _RefBlock = other._RefBlock; other._Ptr = nullptr; other._RefBlock = nullptr; } ~My_Shared_Ptr() { if (_RefBlock && _RefBlock->Decref() == 0) { _Deleter(_Ptr); delete _RefBlock; } } My_Shared_Ptr& operator=(const My_Shared_Ptr& other) { if (this != &other) { if (_RefBlock && _RefBlock->Decref() == 0) { _Deleter(_Ptr); delete _RefBlock; } _Ptr = other._Ptr; _RefBlock = other._RefBlock; if (_RefBlock) _RefBlock->Incref(); } return *this; } My_Shared_Ptr& operator=(My_Shared_Ptr&& other) noexcept { if (this != &other) { if (_RefBlock && _RefBlock->Decref() == 0) { _Deleter(_Ptr); delete _RefBlock; } _Ptr = other._Ptr; _RefBlock = other._RefBlock; other._Ptr = nullptr; other._RefBlock = nullptr; } return *this; } long use_count() const { return _RefBlock ? _RefBlock->use_count() : 0; } pointer get() const { return _Ptr; } void reset(pointer p = nullptr) { if (_RefBlock && _RefBlock->Decref() == 0) { _Deleter(_Ptr); delete _RefBlock; } _Ptr = p; _RefBlock = p ? new My_RefCount<_Ty>(p) : nullptr; } void swap(My_Shared_Ptr& other) { std::swap(_Ptr, other._Ptr); std::swap(_RefBlock, other._RefBlock); } explicit operator bool() const { return _Ptr != nullptr; } _Ty& operator*() const { return *_Ptr; } pointer operator->() const { return _Ptr; } };
|
辅助函数与语义
辅助函数
- get():返回内部裸指针,不转移所有权,严禁手动delete该指针;
- reset():释放当前资源,引用计数-1,可传入新指针重新绑定,无参数则置空;
- use_count():返回当前强引用计数值,多用于调试排查生命周期问题;
- swap():交换两个shared_ptr的资源与控制块,无拷贝、无释放,效率极高;
- operator bool():隐式布尔转换,快速判断指针是否持有有效资源;
- 无release()接口:与unique_ptr区别,不支持手动释放所有权,避免引用计数混乱。
拷贝与移动语义实操
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int main() { shared_ptr<Int> pa(new Int(1)); shared_ptr<Int> pb(pa); shared_ptr<Int> pc(move(pa));
shared_ptr<Int> pd; pd = pb; shared_ptr<Int> pe; pe = move(pd); return 0; }
|
进阶工程用法
动态数组管理
shared_ptr支持数组特化版本,使用时需指定数组类型,自动调用delete[]删除器,C++17及以上版本支持make_shared直接创建数组,严禁用普通shared_ptr管理动态数组,否则会因释放方式不匹配导致程序崩溃。
1 2 3 4 5 6 7
| shared_ptr<Int[]> pArr(new Int[5]);
shared_ptr<Int[]> pArr2 = make_shared<Int[]>(5);
shared_ptr<Int> pErr(new Int[5]);
|
与STL容器结合
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <vector> #include <list> #include <memory> int main() { list<shared_ptr<Int>> intList; intList.emplace_back(make_shared<Int>(12)); intList.emplace_back(make_shared<Int>(23)); vector<shared_ptr<Int>> intVec; intVec.emplace_back(make_shared<Int>(34)); intVec.emplace_back(make_shared<Int>(45)); return 0; }
|
多态与智能指针类型转换
shared_ptr原生支持多态特性,标准库提供专属类型转换函数,替代普通指针的强制转换,同步保障引用计数正常联动,避免生命周期管控异常,核心转换函数如下:
- static_pointer_cast:静态转换,适配派生类转基类(上行转换);
- dynamic_pointer_cast:动态转换,适配基类转派生类(下行转换),自带安全校验;
- const_pointer_cast:移除const属性,对应const_cast。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Animal { public: virtual void eat() = 0; virtual ~Animal() = default; }; class Dog : public Animal { public: void eat() override { cout << "Dog eat bone" << endl; } }; int main() { shared_ptr<Dog> pd = make_shared<Dog>(); shared_ptr<Animal> pa = pd; pa->eat(); shared_ptr<Dog> pd2 = dynamic_pointer_cast<Dog>(pa); if (pd2) pd2->eat(); return 0; }
|
工厂方法模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Shape { public: virtual void draw() = 0; virtual ~Shape() = default; static shared_ptr<Shape> factory(const string& type); }; class Circle : public Shape { public: void draw() override { cout << "Draw Circle" << endl; } }; shared_ptr<Shape> Shape::factory(const string& type) { if (type == "Circle") return make_shared<Circle>(); return nullptr; } int main() { vector<shared_ptr<Shape>> shapes; shapes.emplace_back(Shape::factory("Circle")); for (auto& ptr : shapes) ptr->draw(); return 0; }
|
隐患:循环引用
循环引用是shared_ptr较为常见的内存泄漏诱因,具体指两个或多个对象通过shared_ptr相互持有对方,形成闭环引用关系,导致双方引用计数永远无法归零,资源无法正常释放,这类问题需要搭配weak_ptr弱引用破除闭环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class A; class B; class A { public: shared_ptr<B> pb; ~A() { cout << "Destroy A" << endl; } }; class B { public: shared_ptr<A> pa; ~B() { cout << "Destroy B" << endl; } }; int main() { shared_ptr<A> a = make_shared<A>(); shared_ptr<B> b = make_shared<B>(); a->pb = b; b->pa = a; return 0; }
|
线程安全说明
- 引用计数线程安全:计数增减采用原子操作,多线程并发拷贝、析构shared_ptr,计数不会错乱;
- 托管对象非线程安全:多线程同时读写托管对象,需搭配互斥锁,避免数据竞争;
- 单实例指针非线程安全:多线程同时修改同一个shared_ptr实例(赋值、reset),需加锁保护。
高频易错禁忌
- 避免同一裸指针初始化多个shared_ptr,防止重复释放崩溃;
- 留意循环引用问题,可通过weak_ptr弱引用替代强引用破除闭环;
- 不混用shared_ptr与裸指针管理同一份资源;
- 不手动delete get()返回的裸指针,避免破坏引用计数逻辑;
- 仅管理堆对象,不使用shared_ptr托管栈对象。
适用场景
- 多对象、多模块需要共享同一份资源,且无法确定唯一释放时机的场景;
- STL容器中存储智能指针,替代裸指针与废弃的auto_ptr;
- 多态对象管理、工厂模式返回值,兼顾生命周期安全与多态特性;
- 多线程资源共享(搭配互斥锁保障对象访问安全);
- 资源需要被多次引用、传递,且无需手动管控释放的场景。
循环引用是shared_ptr在工程中常见的内存泄漏场景,两个对象通过shared_ptr相互持有形成闭环后,双方引用计数无法减至0,资源无法释放,搭配weak_ptr即可解决这类问题,具体用法详见下方weak_ptr章节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class A; class B; class A { public: shared_ptr<B> pb; ~A() { cout << "Destroy A" << endl; } }; class B { public: shared_ptr<A> pa; ~B() { cout << "Destroy B" << endl; } };
int main() { shared_ptr<A> a = make_shared<A>(); shared_ptr<B> b = make_shared<B>(); a->pb = b; b->pa = a; return 0; }
|
其常用接口功能清晰直观:use_count用于获取当前对象的引用计数值,unique判断当前指针是否单独持有托管对象,reset重置指针并自动释放原有关联资源,get用于获取内部裸指针(不建议手动修改或提前释放该指针,避免破坏智能指针生命周期管控)。
weak_ptr弱引用智能指针
weak_ptr是C++11标准中专为配合shared_ptr设计的弱引用智能指针,不属于独立的资源管理指针,不具备普通指针的解引用、成员访问能力,核心定位是共享资源的生命周期观测者,而非所有者。其设计的核心目的,就是破解shared_ptr的循环引用难题,同时规避共享场景下的悬空指针非法访问风险,填补shared_ptr共享机制的安全漏洞,是现代C++智能指针体系中不可或缺的配套组件。
底层核心特性
- 不占用强引用计数:weak_ptr绑定shared_ptr后,仅递增控制块内的弱引用计数,绝对不影响shared_ptr的强引用计数,不会延长托管对象的生命周期,这是破解循环引用的核心原理;
- 无资源所有权:weak_ptr不负责对象的创建与释放,既不会接管资源,也不会触发析构释放,全程仅观测对象生命周期,不干预shared_ptr的资源管控逻辑;
- 依赖shared_ptr初始化:weak_ptr无法直接通过new或裸指针初始化,只能通过已有的shared_ptr或其他weak_ptr拷贝、移动初始化,和shared_ptr共用同一个引用计数控制块;
- 多线程安全兼容:弱引用计数的增减同样采用原子操作,和shared_ptr的强引用计数保持一致的线程安全性,多线程场景下可安全观测共享资源;
- 无默认指针运算符:未重载*、->运算符,无法直接访问托管对象,必须先转换为合法的shared_ptr后再操作,从语法层面杜绝悬空访问。
接口全解与实操
weak_ptr的接口数量少、功能专一,全部围绕生命周期观测与安全访问设计,五大核心接口功能明确,适配不同使用场景,具体用法与代码示例如下:
- use_count():获取当前观测对象的强引用计数值,可实时查看有多少个shared_ptr持有该资源,多用于调试排查循环引用、生命周期异常问题;
- expired():判断观测对象是否已被释放,返回bool值,true代表资源已销毁、指针悬空,false代表资源仍有效,是访问前的核心校验步骤;
- lock():将weak_ptr安全转换为shared_ptr,若资源未销毁则返回有效shared_ptr,若已销毁则返回空shared_ptr,这是weak_ptr访问对象的唯一合法方式;
- reset():重置weak_ptr,断开与当前观测资源的关联,弱引用计数递减,不影响强引用计数与资源本身;
- swap():交换两个weak_ptr的观测对象,无资源拷贝、无计数变动,效率极高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| #include <iostream> #include <memory> using namespace std;
class Test { public: Test() { cout << "Test对象创建" << endl; } ~Test() { cout << "Test对象销毁" << endl; } void show() { cout << "Test对象方法调用" << endl; } };
int main() { shared_ptr<Test> sp = make_shared<Test>(); weak_ptr<Test> wp(sp);
cout << "当前强引用计数:" << wp.use_count() << endl;
if (!wp.expired()) { shared_ptr<Test> tmp = wp.lock(); tmp->show(); }
sp.reset(); cout << "是否已过期:" << boolalpha << wp.expired() << endl;
shared_ptr<Test> fail_sp = wp.lock(); if (!fail_sp) { cout << "资源已销毁,无法访问" << endl; } return 0; }
|
循环引用问题
循环引用是shared_ptr较为常见的内存泄漏场景,核心表现为两个及以上对象通过shared_ptr相互持有,形成闭环,导致强引用计数无法归零,资源无法正常释放。weak_ptr通过替换其中一方的强引用为弱引用,打破闭环,是标准的解决方案,对比案例如下:
错误案例:循环引用导致内存泄漏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class A; class B; class A { public: shared_ptr<B> pb; ~A() { cout << "A对象销毁" << endl; } }; class B { public: shared_ptr<A> pa; ~B() { cout << "B对象销毁" << endl; } }; int main() { shared_ptr<A> a = make_shared<A>(); shared_ptr<B> b = make_shared<B>(); a->pb = b; b->pa = a; return 0; }
|
修正案例:weak_ptr破除闭环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class A; class B; class A { public: shared_ptr<B> pb; ~A() { cout << "A对象销毁" << endl; } }; class B { public: weak_ptr<A> pa; ~B() { cout << "B对象销毁" << endl; } }; int main() { shared_ptr<A> a = make_shared<A>(); shared_ptr<B> b = make_shared<B>(); a->pb = b; b->pa = a; return 0; }
|
修正后,B对A的持有变为弱引用,不影响A的强引用计数,当外部shared_ptr释放后,A的强引用计数归零并销毁,进而触发B的强引用计数归零,闭环彻底打破,无内存泄漏。
悬空指针
weak_ptr自带悬空防护能力,是共享资源缓存、观察者模式等场景的优质选择:在多线程或异步场景中,shared_ptr释放资源后,weak_ptr通过expired()能立刻感知资源失效,lock()转换只会得到空指针,绝对不会出现非法访问悬空内存的情况,彻底规避裸指针和单纯shared_ptr无法解决的悬空访问崩溃问题。
高频易错与使用禁忌
- 不直接使用weak_ptr访问对象,建议通过lock()转换为shared_ptr后,校验指针有效性再操作;
- weak_ptr不可单独使用,需依附shared_ptr,无独立资源管理能力;
- 不建议用weak_ptr替代shared_ptr做常规资源共享,仅适用于观测和破除循环引用;
- 循环引用场景中,将其中一方改为weak_ptr即可破除闭环,无需双方替换;
- weak_ptr的expired()判断和lock()转换并非原子操作,多线程场景下建议直接用lock()转换后判空,安全性更高。
选型与工程规范
适用场景区分
unique_ptr、shared_ptr、weak_ptr三者分工明确,适用场景差异鲜明,日常开发需根据业务需求精准选型,避免误用:unique_ptr适配独占式资源管理场景,无需资源共享、追求极致高效内存管控的场景均可优先选用,比如对象内部专属资源、函数内临时资源托管、工厂模式返回值,是日常独占资源管理的首选指针;shared_ptr适配多对象、多模块共享同一资源的场景,无需手动管控生命周期,共享完成后自动释放资源,比如容器存储共享对象、多线程共享资源、跨模块数据传递;weak_ptr仅作为shared_ptr的辅助工具,不单独承担资源管理职责,专门用于破解循环引用、观测对象生命周期,无独立使用场景。
工程开发使用规范
现代C++工程开发中,需遵循以下核心使用规范,最大化发挥智能指针的优势,规避各类内存风险:优先选用C++11智能指针替代传统裸指针,最大限度降低内存泄漏风险;优先通过std::make_unique、std::make_shared创建智能指针,避免手动new与裸指针混用带来的管控漏洞;禁止用同一个裸指针初始化多个shared_ptr,杜绝重复释放崩溃问题;使用shared_ptr时需警惕循环引用,及时搭配weak_ptr破除闭环;unique_ptr禁止常规拷贝,通过移动语义完成所有权转移,不强行实现资源共享;不随意通过get函数获取裸指针并手动释放,避免破坏智能指针的自动化生命周期管控逻辑。
C++11智能指针依托RAII机制与移动语义,搭建起一套完善的自动化内存管理体系,彻底解决了传统裸指针带来的内存泄漏、悬空指针、重复释放等诸多痛点。unique_ptr实现高效无开销的独占式内存管理,shared_ptr实现安全灵活的共享式资源管控,weak_ptr针对性破解循环引用难题,三者协同配合,覆盖现代C++开发全场景的内存管控需求。严格遵循规范选型与使用智能指针,既能大幅降低内存管理的调试成本,又能显著提升程序稳定性与运行安全性,是现代C++开发者必须熟练掌握的核心基础技能。