当前位置: 首页 > news >正文

剖析智能指针shared_ptr实现原理

目录

一、引言

二、基本信息介绍

三、完整等效源码

1.控制块基类

2.控制块实现

3.shared_ptr具体实现

四、使用演示

1.创建构造

2.调用函数


一、引言

(传统裸指针管理方案的痛点与风险,以此引出智能指针)

悬垂指针:‌多线程场景下,访问已释放的内存引发未定义行为,程序崩溃

异常问题:‌若在newdelete之间出现异常,需要更复杂的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);
http://www.xdnf.cn/news/506881.html

相关文章:

  • Devin 编程智能体
  • 2023 睿抗机器人开发者大赛CAIP-编程技能赛-专科组(国赛)解题报告 | 珂学家
  • Active Directory域环境信息收集实战指南
  • 摄影构图小节
  • [逆向工程]C++实现DLL注入:原理、实现与防御全解析(二十五)
  • Flowbite 和 daisyUI 那个好用?
  • AI Agent开发第69课-彻底消除RAG知识库幻觉(3)-手撕“重排序”
  • W5500使用ioLibrary库创建DNS客户端
  • 【人工智能】DeepSeek解码:揭秘AI大模型训练的创新密码
  • 从0到1:Python项目部署与运维全攻略(10/10)
  • 如何在Cursor中高效使用MCP协议
  • 桌面端进程通信
  • 第十一课 蜗牛爬树
  • 恢复因 oh-my-zsh 安装导致丢失的 zsh 环境变量
  • 【Docker 新手入门指南】第五章:Hello Word
  • JavaScript运算符
  • 人工智能-自然语言与语音产品实现
  • SpringBoot--自动配置原理详解
  • 2025.05.17淘天机考笔试真题第二题
  • vue使用axios实现拦截器
  • 体育比分数据服务避坑指南
  • 信息与信息化
  • 【高斯函数拟合】高斯-牛顿法与梯度下降法的 Python 实现
  • Python集合运算:从基础到进阶全解析
  • 无线信道的噪声与干扰
  • 长三角、珠三角、成渝、京津冀四大城市群的区域与分布
  • 生产者 - 消费者模式实现方法整理
  • Ubuntu 添加系统调用
  • 给你的matplotlib images添加scale Bar
  • Python 3.11详细安装步骤(包含安装包)Python 3.11详细图文安装教程