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

【c++】【智能指针】shared_ptr底层实现

【c++】【智能指针】shared_ptr底层实现

智能指针之前已经写过了,但是考虑到不够深入,应该再分篇写写。

1 shared_ptr

1.1 shared_ptr 是什么

std::shared_ptr是一个类模板,它的对象行为像指针,但是它还能记录有多少个对象共享它管理的内存对象
多个std::shared_ptr可以共享同一个对象
最后一个std::shared_ptr被销毁时,它会自动释放它所指向的对象


1.2 shared_ptr创建和销毁

可以通过make_shared<>函数来创建。
可以通过拷贝或赋值另一个shared_ptr来创建
eg:sp1和sp2指向同一个对象,内存对象的引用计数为2。当sp1被销毁时,引用计数减为1,sp2仍然指向该对象。当sp2被销毁时,引用计数减为0,内存对象被销毁。
在这里插入图片描述


1.3 shared_ptr的底层原理

element_type*    _M_ptr;         // 所管理对象的地址.
__shared_count<_Lp>  _M_refcount;    // 引用计数块地址.

在这里插入图片描述
std::shared_ptr在内部其只有两个指针成员:

  • 一个指针是所管理的数据的地址
  • 一个指针是控制块的地址
    包括引用计数
    weak_ptr计数
    删除器(Deleter)
    分配器(Allocator)

因为不同shared_ptr指针需要共享相同的内存对象,因此引用计数的存储是在 上的。
而unique_ptr只有一个指针成员,指向所管理的数据的地址。因此一个shared_ptr对象的大小是它大小的倍。
eg:vs2022-64下:

int main()
{
    std::cout << sizeof(std::shared_ptr<int>) << std::endl; // 8
    std::cout << sizeof(std::unique_ptr<int>) << std::endl; // 4
}

在这里插入图片描述


1.4 std::shared_ptr的简单实现

我们通过下面这个简单的类来模拟std::shared_ptr<>的实现,来理解引用计数的实现原理。
这里我们为了简单,只实现了

  • shared_ptr的拷贝构造函数析构函数赋值运算符函数引用计数只是简单地用了一个int类型的内存空间
  • 省略了weak_ptr的计数、删除器和分配器,不考虑多线程的情况。
  • 当我们销毁一个shared_ptr时,引用计数减1。当引用计数减为0时,我们删除指向实际数据的指针和指向引用计数的指针。
  • 当我们拷贝一个shared_ptr时,引用计数加1。
  • 当我们赋值一个shared_ptr时,我们首先递减左侧运算对象的引用计数。如果引用计数变为0,我们就释放左侧运算对象分配的内存以及引用计数的内存。然后拷贝右侧运算对象的数据指针和引用计数指针,最后递增引用计数。
template<typename T>
class shared_ptr {
public:
    // 构造函数  
    // 初始化智能指针,传入一个裸指针(默认为 nullptr)  
    shared_ptr(T* ptr = nullptr) : m_ptr(ptr), m_refCount(new int(1)) {}

    // 拷贝构造函数  
    // 通过另一个 shared_ptr 构造,增加引用计数  
    shared_ptr(const shared_ptr& other) : m_ptr(other.m_ptr), m_refCount(other.m_refCount) {
        // 增加引用计数  
        (*m_refCount)++;
    }

    // 析构函数  
    ~shared_ptr() {
        // 减少引用计数  
        (*m_refCount)--;
        // 如果引用计数为 0,释放内存  
        if (*m_refCount == 0) {
            delete m_ptr;
            delete m_refCount;
        }
    }

    // 重载赋值运算符  
    shared_ptr& operator=(const shared_ptr& other) {
        // 检查自我赋值  
        if (this != &other) {
            // 减少旧对象的引用计数  
            (*m_refCount)--;
            // 如果引用计数为 0,释放内存  
            if (*m_refCount == 0) {
                delete m_ptr;
                delete m_refCount;
            }
            // 复制新对象的数据和引用计数指针,并增加引用计数  
            m_ptr = other.m_ptr;
            m_refCount = other.m_refCount;
            // 增加引用计数  
            (*m_refCount)++;
        }
        return *this;
    }

private:
    T* m_ptr;            // 指向实际数据的指针  
    int* m_refCount;     // 引用计数  
};

ps:多线程时 引用计数的++操作需要是原子性的。考虑使用std::atomic

  • 是 C++11 引入的模板类,位于头文件 中,主要用于在多线程环境下实现原子操作,从而避免数据竞争(data race),保证线程安全。

1.5 什么时候用 std::shared_ptr<T>

std::shared_ptr<T> 主要用于以下场景:
1. 资源创建昂贵、比较耗时的场景

  • 创建对象代价很高(例如文件、网络、数据库等),不希望频繁创建和销毁。
  • 通过共享指针来管理对象生命周期,避免频繁创建和释放导致的性能损耗。

示例:管理数据库连接

#include <iostream>
#include <memory>

class Database {
public:
    Database() { std::cout << "Connecting to Database\n"; }
    ~Database() { std::cout << "Closing Database Connection\n"; }
};

void useDatabase(std::shared_ptr<Database> db) {
    std::cout << "Using Database\n";
}

int main() {
    auto db = std::make_shared<Database>(); // 资源创建昂贵,使用共享指针管理
    useDatabase(db); // 共享所有权
}

在这个例子中,Database连接创建和释放都很昂贵,使用 shared_ptr 可以在多个对象间安全地共享连接(并非多线程),等所有使用者都释放之后才会关闭连接。

  • std::shared_ptr` 的引用计数是线程安全的(即对引用计数的增加和减少是原子操作,不会导致竞争条件)
  • 对实际管理的对象的操作是非线程安全的
    ps: 之前的文章有提到

在示例中,以下部分是线程安全的:

  • 引用计数的增加和减少
  • 判断对象是否需要释放
  • 只读访问是线程安全的

需要加锁的部分:

  • 对实际对象(Database)的数据修改需要加锁。
    如果多个线程同时修改 Database 对象的内容,可能会发生数据竞争,导致未定义行为。因此,需要在访问或修改对象时加锁

2. 需要共享资源的所有权

  • 一个对象的生命周期可能同时涉及多个对象,但不清楚谁会最终释放这个对象。
  • std::shared_ptr 使用引用计数,确保在最后一个 shared_ptr 离开作用域时才释放资源。

示例:对象被多个对象共享

#include <iostream>
#include <memory>

class A {
public:
    A() { std::cout << "A constructed\n"; }
    ~A() { std::cout << "A destroyed\n"; }
};

int main() {
    std::shared_ptr<A> sp1 = std::make_shared<A>();
    std::shared_ptr<A> sp2 = sp1; // 引用计数 +1
    std::shared_ptr<A> sp3 = sp2; // 引用计数 +1

    std::cout << "Use count: " << sp1.use_count() << "\n"; // 输出 3
    sp2.reset(); // 引用计数 -1
    std::cout << "Use count after sp2 reset: " << sp1.use_count() << "\n"; // 输出 2
    sp1.reset(); // 引用计数 -1
    std::cout << "Use count after sp1 reset: " << sp3.use_count() << "\n"; // 输出 1
    sp3.reset(); // 最终释放对象
}
  • sp1, sp2, sp3 共享对 A 对象的所有权。
  • 只有最后一个 shared_ptr 释放时,才会析构 A 对象。

不适用 shared_ptr的情况
1. 存在明显的所有者 → 使用 unique_ptr 更合适。
2. 不需要共享所有权 → 使用裸指针或 unique_ptr
3. 循环引用问题 → 使用 weak_ptr 解决。

部分转自:https://zhuanlan.zhihu.com/p/672745555?utm_source=chatgpt.com

相关文章:

  • python_巨潮年报pdf下载
  • 判断是不是二叉搜索树(C++)
  • java静态变量,静态方法存储在内存中哪个位置
  • TCP怎么保证可靠传输
  • redis常用命令
  • Sublime Text 2.0.2 安装与汉化指南:从下载到中文包配置的完整教程
  • 【强化学习】第二讲——探索与利用exploration vs. exploitation
  • [WEB开发] Web基础
  • zero-shot文字分类模型
  • 【数据结构与算法】Java描述:第四节:二叉树
  • 苹果app上架app store 之苹果开发者账户在mac电脑上如何使用钥匙串访问-发行-APP发布证书ios_distribution.cer-优雅草卓伊凡
  • DeepSeek 3FS集群化部署临时笔记
  • Django中的查询条件封装总结
  • 解决 openjtalk.obj : error LNK2001: 无法解析的外部符号 __imp__PySequence_List 错误
  • C语言基础要素(016):入口条件循环:while与for
  • go 通过汇编分析栈布局和函数栈帧
  • SSM文物管理系统
  • chatgpt的一些prompt技巧
  • vue3设置全局滚动条样式
  • 1.5[hardware][day5]
  • 短剧迷|《权宠》一出,《名不虚传》
  • 视频丨英伟达总裁黄仁勋:美勿幻想AI领域速胜中国
  • “译通天下·言立寰宇”:华东师大翻译家的精神传承
  • 夜读丨春天要去动物园
  • 十二届上海市委第六轮巡视全面进驻,巡视组联系方式公布
  • 上海科创再出发:“造星”的城和“摘星”的人