代码仓库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; // 单个对象用delete释放
}
};

// 数组特化删除器:动态数组释放
template<class _Ty>
struct default_deleter<_Ty[]> {
void operator()(_Ty* ptr) const {
delete[] ptr; // 数组必须用delete[]释放
}
};

// 自定义文件删除器:适配FILE*非堆内存资源释放
struct DelFile {
void operator()(FILE* fp) const {
if (fp != nullptr) { // 增加空指针校验,避免异常
fclose(fp);
}
}
};

// unique_ptr主体模板类
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;
}

// 交换两个unique_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];
}

// 基础功能函数:release/reset/swap/get/operator bool 逻辑同普通版本
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置空
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));
// 移动构造:所有权转移给pIntb,pInta置空
unique_ptr<Int> pIntb(std::move(pInta));
// 移动赋值:所有权转移给pIntc,pIntb置空
unique_ptr<Int> pIntc;
pIntc = std::move(pIntb);

// 函数返回unique_ptr,自动触发移动语义,合法
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[]>
unique_ptr<Int[]> pArr(new Int[5]{1,2,3,4,5});
// 通过[]运算符访问数组元素
pArr[0] = 10;
pArr[1] = 20;
// 离开作用域,自动调用delete[]释放数组,无内存泄漏

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) {
// 完美转发参数,调用对应构造函数,返回构造好的unique_ptr
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
// 1. 默认构造:无参数传递
auto pEmpty = make_unique<Int>();
// 2. 单参构造:传递int类型参数
auto pInt = make_unique<Int>(10);
// 3. 多参构造:传递多个不同类型参数
auto pStu = make_unique<Student>(1001, "张三", 20);
// 4. 动态数组创建:指定数组长度
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 {
// 重载()运算符,接收FILE*资源,实现自动关闭
void operator()(FILE* fp) const {
// :空指针防护,避免空指针调用fclose触发异常
if (fp != nullptr) {
fclose(fp); // 调用标准文件关闭API,精准释放资源
fp = nullptr; // 置空指针,规避悬空指针风险
}
}
};

// 工厂函数:创建绑定文件删除器的unique_ptr,封装文件打开逻辑
// 返回值:独占式文件智能指针,离开作用域自动关闭文件
unique_ptr<FILE, DelFile> open_file(const char* filename, const char* mode) {
// 参数合法性校验,避免非法文件名/打开模式
if (filename == nullptr || mode == nullptr) {
return unique_ptr<FILE, DelFile>(nullptr);
}
// 调用C标准库打开文件,失败返回nullptr
FILE* fp = fopen(filename, mode);
// 构造并返回绑定自定义删除器的unique_ptr
return unique_ptr<FILE, DelFile>(fp);
}

// 完整调用示例:无手动fclose,全自动资源管理
void test_file_manage() {
// 以只读方式打开文件,获取智能指针
auto file_ptr = open_file("test.txt", "r");
// 利用operator bool判断文件是否成功打开
if (!file_ptr) {
printf("文件打开失败,请检查文件路径或权限!\n");
return;
}

// 文件读写操作:通过get()获取原生FILE*,不转移所有权
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);
}

// 优势:无需手动调用fclose,函数退出、指针析构时自动关闭文件
// 即便函数中途return、抛出异常,也能保证资源正常释放,无句柄泄漏
}

// 写入文件场景示例:同样复用DelFile删除器
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;
}
}
};
// 使用方式:unique_ptr<SOCKET, DelSocket> sock_ptr(new SOCKET(socket(AF_INET, SOCK_STREAM, 0)));

线程互斥锁资源

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
// 错误示例:unique_ptr作为unordered_map键,编译+运行双重报错
#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() {
// 定义哈希容器,key为size_t,value为unique_ptr<Shape>(value存储合法)
std::unordered_map<size_t, std::unique_ptr<Shape>> shape_map;
std::unique_ptr<Circle> cir = std::make_unique<Circle>();

// 错误行1:尝试用unique_ptr计算哈希值,无对应hash特化,编译报错
// 错误行2:std::move转移所有权后,cir变为悬空指针,键失效
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"); }
};

// 宿主类,通过unique_ptr托管专属组件
class Student {
private:
// 独占式托管Computer资源,专属当前Student对象
unique_ptr<Computer> pc;
public:
// 构造函数中通过make_unique初始化,异常安全、无裸指针暴露
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();
// 函数退出,stu对象析构,pc自动释放Computer资源,无需手动处理
}

托管专属资源的类建议禁用拷贝,避免多个对象共享同一独占资源;优先通过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,托管临时数据库连接
unique_ptr<MyConn> pConn(new MyConn());
// 尝试打开连接
bool conn_flag = pConn->openConn(db_addr);

// 分支1:连接失败,提前return,unique_ptr自动释放资源
if (!conn_flag) {
printf("数据库连接失败,函数退出\n");
return;
}

// 分支2:连接成功,执行业务逻辑
pConn->execQuery("select * from user");

// 分支3:模拟业务异常,提前退出
if (db_addr == nullptr) {
throw std::invalid_argument("数据库地址非法");
}

// 正常退出,函数结束后pConn析构,释放连接
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;
// 工厂静态方法,返回unique_ptr,传递所有权
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
unique_ptr<Shape> Shape::factory(const std::string& type) {
if (type == "Circle") {
// C++14及以上,优先make_unique
return make_unique<Circle>();
}
if (type == "Square") {
return make_unique<Square>();
}
// 未知类型返回空指针
return nullptr;
}

// 调用方使用示例
void test_factory() {
// 通过工厂获取对象,接收unique_ptr,获得所有权
auto circle = Shape::factory("Circle");
auto square = Shape::factory("Square");

// 多态调用
if (circle) circle->draw();
if (square) square->draw();

// 函数退出,circle和square自动析构,释放对应派生类对象
// 无需手动delete,无内存泄漏,所有权清晰
}

工厂方法严禁返回裸指针,统一返回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
// 特殊构造类:私有构造,禁止外部直接new
class SpecialTool {
private:
// 私有构造函数,外部无法直接创建对象
SpecialTool() { printf("特殊工具类初始化\n"); }
// 禁用拷贝与移动
SpecialTool(const SpecialTool&) = delete;
SpecialTool& operator=(const SpecialTool&) = delete;

// 友元声明,允许静态创建函数返回unique_ptr
friend unique_ptr<SpecialTool> createSpecialTool();

public:
~SpecialTool() { printf("特殊工具类资源释放完成\n"); }
// 业务方法
void doWork() { printf("执行特殊工具业务逻辑\n"); }
};

// 静态创建函数,唯一对外创建入口,返回unique_ptr
unique_ptr<SpecialTool> createSpecialTool() {
// 内部调用私有构造,创建对象
return unique_ptr<SpecialTool>(new SpecialTool());
}

// 单例模式改良版(受控单例),unique_ptr托管
class Singleton {
private:
Singleton() { printf("单例对象初始化\n"); }
~Singleton() { printf("单例对象释放完成\n"); }
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

public:
// 获取单例实例,返回unique_ptr引用,不转移所有权
static unique_ptr<Singleton>& getInstance() {
static unique_ptr<Singleton> instance(new Singleton());
return instance;
}
// 业务方法
void run() { printf("单例业务执行\n"); }
};

// 调用示例
void test_special_obj() {
// 特殊对象:通过静态函数创建,unique_ptr托管
auto tool = createSpecialTool();
if (tool) tool->doWork();

// 单例对象:unique_ptr托管,程序退出时自动释放
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,强引用计数=1
shared_ptr<Int> pInta(new Int(10));
cout << "pInta 引用计数:" << pInta.use_count() << endl;

// 拷贝构造,计数+1=2
shared_ptr<Int> pIntb(pInta);
cout << "pInta/pIntb 引用计数:" << pInta.use_count() << endl;

// 赋值操作,计数+1=3
shared_ptr<Int> pIntc;
pIntc = pInta;
cout << "当前引用计数:" << pInta.use_count() << endl;

// 函数退出,所有指针依次析构,计数逐次-1,归零后释放资源
return 0;
}

标准创建规范

shared_ptr的创建分为安全首选方案与特殊场景备选方案,工程开发中需严格遵守规范,杜绝各类内存风险,具体创建方式与注意事项如下:

首选方案:std::make_shared(补充缓存局部性优势)

make_shared是标准库提供的专属工厂函数,可一次性分配对象内存与引用计数控制块,将对象数据与引用计数在堆上连续存储,这带来了两大关键优势:

  1. 减少内存分配次数与碎片:单次分配同时完成对象与控制块的内存申请,相比 new + shared_ptr 构造的两次分配,显著降低了内存碎片,提升了内存管理效率,同时异常安全性拉满,完全规避裸指针暴露风险。

  2. 提升缓存局部性(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
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;

// 1. 通用删除器+数组特化删除器
template <typename _Ty>
struct My_Shared_Deleter {
void operator()(_Ty* ptr) const { delete ptr; }
};
// 数组特化,调用delete[]释放动态数组
template <typename _Ty>
struct My_Shared_Deleter<_Ty[]> {
void operator()(_Ty* ptr) const { delete[] ptr; }
};

// 2. 原子引用计数控制块(多线程安全)
template <typename _Ty>
class My_RefCount {
private:
_Ty* _Ptr;
atomic<int> _Uses; // 强引用计数,原子操作
atomic<int> _Weaks; // 弱引用计数,适配weak_ptr
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; }
};

// 3. shared_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);
}
// 拷贝构造:共享资源,计数+1
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;
}
// 析构:计数-1,归零则释放资源
~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));
// 拷贝构造:合法,计数+1
shared_ptr<Int> pb(pa);
// 移动构造:合法,pa置空,计数不变
shared_ptr<Int> pc(move(pa));
// pa已悬空,禁止访问
// pa->PrintInt(); 运行崩溃

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]);
// C++17及以上make_shared创建数组
shared_ptr<Int[]> pArr2 = make_shared<Int[]>(5);

// 错误用法:普通shared_ptr管理数组,释放方式不匹配易导致崩溃
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
list<shared_ptr<Int>> intList;
intList.emplace_back(make_shared<Int>(12));
intList.emplace_back(make_shared<Int>(23));
// vector存储shared_ptr
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;
// 主函数退出,计数均为1,资源无法释放,内存泄漏
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;
// 主函数结束,计数均为1,无法释放,内存泄漏
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() {
// 1. 创建shared_ptr,初始化weak_ptr
shared_ptr<Test> sp = make_shared<Test>();
weak_ptr<Test> wp(sp); // 仅弱引用计数+1,强引用仍为1

// 2. 查看强引用计数
cout << "当前强引用计数:" << wp.use_count() << endl;

// 3. 安全访问:先判断是否过期,再lock转换
if (!wp.expired()) {
shared_ptr<Test> tmp = wp.lock();
tmp->show();
}

// 4. 释放原shared_ptr,资源销毁
sp.reset();
cout << "是否已过期:" << boolalpha << wp.expired() << endl;

// 5. 过期后lock返回空指针,无法访问
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; // 强引用,持有B
~A() { cout << "A对象销毁" << endl; }
};
class B {
public:
shared_ptr<A> pa; // 强引用,持有A,形成闭环
~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;
// 退出作用域,强引用计数均为1,永不归零,内存泄漏
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++开发者必须熟练掌握的核心基础技能。