代码仓库shanchuann/CPP-Learninng

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用于获取内部裸指针(不建议手动修改或提前释放该指针,避免破坏智能指针生命周期管控)。