剖析智能指针shared_ptr实现原理
目录
一、引言
二、基本信息介绍
三、完整等效源码
1.控制块基类
2.控制块实现
四、使用演示
1.创建构造
2.调用函数
一、引言
(传统裸指针管理方案的痛点与风险,以此引出智能指针)
悬垂指针:多线程场景下,访问已释放的内存引发未定义行为,程序崩溃
异常问题:若在
new
和delete
之间出现异常,需要更复杂的try-catch
结构才能保证资源释放内存泄漏:使用new从堆区获取的内存空间,不释放导致内存泄漏,晚释放导致内存被持续占用
管理问题:多个指针可能指向同一资源,但无法判断哪个指针负责释放;当多个指针在不同作用域间传递时,没办法确定资源何时能应该释放
二、基本信息介绍
(shared_ptr是什么、什么功能、应用场景、优点)
shared_ptr是C++标准库提供的智能指针,通过原子类型的引用计数机制,实现共享所有权内存管理。其核心功能包括:①多个指针共享同一对象;②引用计数归零时自动释放内存;③支持自定义删除器。常见的应用场景是多指针共享数据、线程间资源共享、异步回调保持对象存活等场景。相比裸指针,优势在于自动内存管理(避免泄漏)、线程安全的引用计数、明确的资源所有权语义
三、完整等效源码
1.控制块基类
1)代码
#include <atomic> // 原子操作保证线程安全
#include <utility> // 用于std::forward// 控制块基类(类型擦除技术处理不同删除器)
struct ControlBlockBase {std::atomic<size_t> shared_count; // 共享引用计数(原子操作)std::atomic<size_t> weak_count; // 弱引用计数(用于weak_ptr)ControlBlockBase() : shared_count(1), weak_count(0) {} // 构造函数virtual ~ControlBlockBase() = default; // 虚析构函数,才能帮助子类对象析构virtual void delete_resource() = 0; // 纯虚函数,用于删除资源,要求子类一定要实现
};
2)QA
Q:为什么两个引用计数器要使用atomic类型?
A:保证多线程环境下引用计数修改的原子性,防止数据竞争导致计数错误。(反例:甲乙两人同时使用同一个计数器,甲读值为3,乙修改值变4,甲误以为是3,实际为4)
Q:为什么要使用虚析构函数?
A:确保通过基类指针删除子类对象时能正确调用子类析构函数,避免内存泄漏。
(补充:这是C++实现多态的核心原理,以后单独一篇文章解释)
Q:为什么基类指针能够指向子类对象,从而调用子类对象的方法?
A:这就是运行时多态,是虚函数机制特性(补充:这是C++实现多态的核心原理,以后单独一篇文章解释)
Q:为什么要释放资源函数delete_resource选择纯虚函数类型?A:这是对控制块基类的子类的强制实现约束,约束并强迫子类一定要实现资源释放函数
Q:控制块基类在整个程序中作用是什么?(后面再解释)
2.控制块实现
1)代码
// 具体控制块实现(包含删除器和分配器)
template<typename T, typename Deleter>
struct ControlBlock : public ControlBlockBase {T* ptr; // 原始指针Deleter deleter; // 自定义删除器//构造ControlBlock(T* p, Deleter d) : ptr(p), deleter(d) {}//销毁void delete_resource() override {if (ptr) {deleter(ptr); // 调用自定义删除器ptr = nullptr;}}
};
2) QA
Q:什么是删除器?作用是什么?
A:它是自定义资源释放逻辑的函数,因为存在非基础数据类型的复杂资源,编译器无法自动销毁时,就需要我们自己手动自定义删除器,类型是Deleter
Q:如果没有自定义删除器传入,deleter该怎么初始化?A:若未显式指定删除器,
shared_ptr
会默认初始化deleter
为std::default_delete<T>
3)难点
问题:基类与控制块之间是什么关系?
回答:(原理+推断) (接口思想,类型擦除,运行时多态)
①是父子关系,因此基类指针可以指向控制块,因此控制块也拥有和父亲一样的虚函数表。
②控制块实现了基类纯虚函数,因此基类指针实现了运行时多态,可以调用控制块的成员函数。③控制块是模板类,因此任何一种<T, Deleter> 组合就是一个类型,一个独立的类,而不管控制块是什么类型,基类永远是它的父亲,基类指针永远能够管理和操作控制块。
④一个基类可以管理多个控制块,即一对多的关系。
结论:基类指针能无视控制块到底是什么类型,无视控制块成员函数实现细节,都能调用和管理
问题:控制块与具体实现类是什么关系?
回答:①因为控制块实现了基类,而且是父子关系,控制块继承了基类的所有成员变量和成员函数,因此控制块中是包含两个计数器:shared_count,weak_count的,用于记录引用次数。
②控制块负责正确销毁
shared_ptr<T>
对象。
③控制块 / 指向资源的指针ptr,职责是让删除器知道释放什么资源。④具体实现类 / 指向资源的指针ptr ,职责是让用户可以直接访问被管理资源。
⑤一个控制块可以管理多个
shared_ptr<T>
具体实现类,即一对多的关系。结论:控制块负责管理多个具体实现类对象的销毁工作,引用计数工作,资源释放工作
3.shared_ptr具体实现
1)代码
// 简化版shared_ptr实现
template<typename T>
class SharedPtr {
private:T* ptr; // 指向被管理的资源ControlBlockBase* ctrl; // 类型是基类指针,作用是指向控制块,原因是为了实现运行时多态// 释放资源并减少引用计数void release() {if (!ctrl) return;// 减少共享引用计数if (--ctrl->shared_count == 0) {ctrl->delete_resource(); // 删除资源// 如果弱引用也为0,删除控制块if (ctrl->weak_count == 0) {delete ctrl;}}ptr = nullptr;ctrl = nullptr;}public:// 默认构造函数(空指针)SharedPtr() : ptr(nullptr), ctrl(nullptr) {}// 原始指针构造函数template<typename Deleter = std::default_delete<T>>explicit SharedPtr(T* p, Deleter d = Deleter{}): ptr(p), ctrl(new ControlBlock<T, Deleter>(p, d)) {}// 拷贝构造函数SharedPtr(const SharedPtr& other): ptr(other.ptr), ctrl(other.ctrl) {if (ctrl) {++ctrl->shared_count;}}// 移动构造函数SharedPtr(SharedPtr&& other) noexcept: ptr(other.ptr), ctrl(other.ctrl) {other.ptr = nullptr;other.ctrl = nullptr;}// 析构函数~SharedPtr() {release();}// 拷贝赋值SharedPtr& operator=(const SharedPtr& other) {if (this != &other) {release(); ptr = other.ptr;ctrl = other.ctrl;if (ctrl) {++ctrl->shared_count;}}return *this;}// 移动赋值SharedPtr& operator=(SharedPtr&& other) noexcept {if (this != &other) {release();ptr = other.ptr;ctrl = other.ctrl;other.ptr = nullptr;other.ctrl = nullptr;}return *this;}// 解引用操作符T& operator*() const { return *ptr; }T* operator->() const { return ptr; }// 获取原始指针T* get() const { return ptr; }// 引用计数(调试用)size_t use_count() const {return ctrl ? ctrl->shared_count.load() : 0;}// 重置指针void reset() {release();}//重置资源指针,删除器指针template<typename U, typename Deleter = std::default_delete<U>>void reset(U* p, Deleter d = Deleter{}) {release();ptr = p;ctrl = new ControlBlock<U, Deleter>(p, d);}
};
2)QA
Q:实现类中两个指针的作用分别是什么?
A:第一个T* ptr,指向别管理资源,职责是方便用户访问资源;第二个ControlBlockBase* ctrl,实际类型是基类指针,实际指向管理 / 具体实现类的对象 / 的所属控制块,职责是绑定所属控制块。
Q:实现类中的ptr和控制块中的ptr指针,职责有什么不一样A:虽然两个ptr都指向被管理的资源,但是职责分离,分别是实现类ptr负责提供用户访问资源的工具渠道,控制块ptr负责告诉删除器要释放的资源是谁。当然可以通过控制块ptr去访问资源,
这样做虽是合法的,但是违背了职责分离的设计原则,最安全推荐的方式是通过实现类ptr。
四、使用演示
通过理解shared_ptr等效代码,我们能够知道它到底有哪些功能,性质,实现原理是怎样的,那现在我们开始按照使用环节进行演示如何使用,我们首先给出代码壳子
#include <iostream>
#include <memory>
#include <vector>class Demo {
public:Demo(int id) : id(id) { std::cout << id << "号对象已创建\n"; }~Demo() { std::cout << id << "号对象已创建\n"; }void show() const { std::cout << "当前对象ID: " << id << "\n"; }int id;
};int main(){//一.创建构造演示//二.函数调用演示//三.销毁释放演示return 0;}
1.创建构造
以下是两种创建shared_ptr指针对象的方式,分别是原生指针构造,make_shared构造。区别如下:
- 原生指针是两次内存分配,先分配资源对象内存,再分配控制块,可以自定义删除器。
- make_shared构造是一次内存分配,一次完成对象内存和控制块,不可以自定义删除器。
// 1. 默认构造(空指针)std::shared_ptr<Resource> p1;std::cout << "p1引用计数: " << p1.use_count() << "\n";// 2. 原生指针构造std::shared_ptr<Resource> p2(new Resource(2));p2->id = 100;// 3. make_shared构造(推荐方式)auto p3 = std::make_shared<Resource>(3);// 4. 拷贝构造(p3在,p4在,引用计数+1)auto p4(p3);std::cout << "p3引用计数: " << p3.use_count() << "\n";// 5. 移动构造(p5在,p2不存在,引用计数不变)auto p5(std::move(p2));std::cout << "p2是否为空: " << (p2 ? "否" : "是") << "\n";
2.调用函数
// 1.操作符重载if (p1) { // 如果p1不为空(*p1).show(); // 重载operator* p1->show(); // 重载operator-> }// 2.重置指针std::cout << "\np1引用计数: " << p1.use_count() << "\n";p1.reset(new Demo(3)); // 重置std::cout << "\n重置后引用计数: " << p1.use_count() << "\n";// 3.交换与比较std::cout << "\n交换前p1和p2,值分别为: " << p1.show() << p2.show() << "\n";p1.swap(p2);std::cout << "\n交换后p1和p2,值分别为: " << p1.show() << p2.show() << "\n";// 4.自定义删除器auto deleter = [](Demo* p) {std::cout << "调用自定义删除器\n";delete p;};std::shared_ptr<Demo> p6(new Demo(6), deleter);