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

C++11 shared_ptr 原理与详细教程

文章目录

    • 一、shared_ptr
      • 核心优势
    • 二、shared_ptr 原理
      • 2.1 引用计数机制
      • 2.2 控制块结构
      • 2.3 内存布局
      • 2.4 简化版 shared_ptr 实现
      • 2.5 关键技术点解析
    • 三、shared_ptr 使用详解
      • 3.1 基本用法
      • 3.2 自定义删除器
      • 3.3 数组支持 (C++17)
      • 3.4 与 weak_ptr 配合使用
    • 四、高级应用场景
      • 4.1 多线程共享资源
      • 4.2 作为容器元素
      • 4.3 类型转换
    • 五、注意事项与最佳实践
      • 5.1 避免的操作
      • 5.2 最佳实践
    • 六、shared_ptr 与 unique_ptr 对比
    • 七、总结

一、shared_ptr

std::shared_ptr 是 C++11 引入的另一种智能指针,与 unique_ptr 的独占所有权不同,它实现了共享所有权语义。多个 shared_ptr 实例可以同时管理同一个对象,当最后一个持有该对象的 shared_ptr 被销毁时,对象才会被自动释放。这种机制通过引用计数(reference counting)实现,是解决资源共享问题的理想选择。

核心优势

  • 共享所有权:支持多指针共同管理同一资源
  • 自动释放:最后一个所有者销毁时自动释放资源
  • 灵活性:可与标准容器和算法无缝配合
  • 线程安全:引用计数的修改是原子操作,支持多线程环境
  • 自定义删除器:支持自定义资源释放逻辑

二、shared_ptr 原理

2.1 引用计数机制

shared_ptr 的核心是引用计数(reference count):

  • 每个被管理的对象都有一个关联的引用计数器,记录当前有多少个 shared_ptr 指向它
  • 当创建新的 shared_ptr 拷贝时,引用计数增加 1
  • shared_ptr 被销毁或重置时,引用计数减少 1
  • 当引用计数变为0时,对象被自动删除

2.2 控制块结构

shared_ptr 内部通过控制块(control block)管理引用计数和其他资源。一个典型的控制块包含:

  • 共享引用计数:跟踪管理对象的 shared_ptr 数量
  • 弱引用计数:跟踪观察对象的 weak_ptr 数量(后续讲解)
  • 删除器:用于释放管理对象的函数或函数对象
  • 分配器:用于内存管理的分配器
  • 管理对象指针:指向实际管理的对象

2.3 内存布局

shared_ptr 通常包含两个指针大小的成员:

  1. 指向管理对象的指针(get() 返回的值)
  2. 指向控制块的指针

![shared_ptr内存布局示意图]

当使用 std::make_shared 创建 shared_ptr 时,控制块和管理对象会在同一块内存中分配,减少内存碎片并提高缓存效率。

2.4 简化版 shared_ptr 实现

下面通过实现简化版 shared_ptr 理解其工作原理:

#include <atomic>
#include <utility>// 前向声明
template <typename T>
class WeakPtr;// 控制块基类
class ControlBlockBase {
public:std::atomic<int> shared_count;  // 共享引用计数std::atomic<int> weak_count;    // 弱引用计数ControlBlockBase() : shared_count(1), weak_count(0) {}virtual ~ControlBlockBase() = default;virtual void destroy() = 0;     // 销毁管理对象virtual void deallocate() = 0;  // 释放控制块
};// 针对原始指针的控制块
template <typename T, typename Deleter>
class ControlBlock : public ControlBlockBase {
private:T* ptr;          // 管理对象指针Deleter deleter; // 删除器public:ControlBlock(T* p, Deleter d) : ptr(p), deleter(std::move(d)) {}void destroy() override {deleter(ptr); // 使用删除器销毁对象}void deallocate() override {delete this;  // 释放控制块自身}
};// 简化版 shared_ptr 实现
template <typename T>
class SharedPtr {
private:T* ptr;                  // 存储的指针ControlBlockBase* control_block; // 控制块指针// 增加引用计数void increase_count() {if (control_block) {control_block->shared_count.fetch_add(1, std::memory_order_relaxed);}}// 减少引用计数并检查是否需要销毁void decrease_count() {if (control_block) {if (control_block->shared_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {control_block->destroy();       // 销毁管理对象if (control_block->weak_count == 0) {control_block->deallocate(); // 释放控制块}}}}public:// 默认构造函数SharedPtr() : ptr(nullptr), control_block(nullptr) {}// 接受原始指针和删除器的构造函数template <typename Deleter = std::default_delete<T>>explicit SharedPtr(T* p, Deleter deleter = Deleter()) {if (p) {ptr = p;control_block = new ControlBlock<T, Deleter>(p, std::move(deleter));} else {ptr = nullptr;control_block = nullptr;}}// 拷贝构造函数SharedPtr(const SharedPtr& other) : ptr(other.ptr), control_block(other.control_block) {increase_count();}// 移动构造函数SharedPtr(SharedPtr&& other) noexcept : ptr(other.ptr), control_block(other.control_block) {other.ptr = nullptr;other.control_block = nullptr;}// 析构函数~SharedPtr() {decrease_count();}// 拷贝赋值运算符SharedPtr& operator=(const SharedPtr& other) {if (this != &other) {decrease_count();          // 减少当前引用计数ptr = other.ptr;control_block = other.control_block;increase_count();          // 增加新的引用计数}return *this;}// 移动赋值运算符SharedPtr& operator=(SharedPtr&& other) noexcept {if (this != &other) {decrease_count();          // 减少当前引用计数ptr = other.ptr;control_block = other.control_block;other.ptr = nullptr;other.control_block = nullptr;}return *this;}// 解引用运算符T& operator*() const {return *ptr;}// 成员访问运算符T* operator->() const {return ptr;}// 数组访问运算符 (C++17)T& operator[](size_t index) const {return ptr[index];}// 获取原始指针T* get() const {return ptr;}// 获取引用计数long use_count() const {if (control_block) {return control_block->shared_count.load(std::memory_order_relaxed);}return 0;}// 检查是否是唯一所有者bool unique() const {return use_count() == 1;}// 显式转换为 boolexplicit operator bool() const {return ptr != nullptr;}// 重置指针template <typename Deleter = std::default_delete<T>>void reset(T* p = nullptr, Deleter deleter = Deleter()) {SharedPtr<T>(p, std::move(deleter)).swap(*this);}// 交换指针void swap(SharedPtr& other) noexcept {std::swap(ptr, other.ptr);std::swap(control_block, other.control_block);}// 友元类声明friend class WeakPtr<T>;
};// 辅助函数:创建 shared_ptr
template <typename T, typename... Args>
SharedPtr<T> make_shared(Args&&... args) {return SharedPtr<T>(new T(std::forward<Args>(args)...));
}

2.5 关键技术点解析

  1. 引用计数的原子操作

    • 使用 std::atomic 确保引用计数的线程安全
    • 增加计数使用 memory_order_relaxed(仅需原子性)
    • 减少计数使用 memory_order_acq_rel(确保资源释放的正确顺序)
  2. 控制块的多态设计

    • 基类 ControlBlockBase 定义通用接口
    • 派生类 ControlBlock 处理具体类型和删除器
    • 支持不同类型的删除器和分配器
  3. 拷贝与移动语义

    • 拷贝操作增加引用计数
    • 移动操作转移所有权,不修改引用计数
    • 析构时减少计数,为0时销毁对象

三、shared_ptr 使用详解

3.1 基本用法

#include <memory>
#include <iostream>struct MyClass {MyClass(int id) : id(id) { std::cout << "MyClass(" << id << ") constructed\n"; }~MyClass() { std::cout << "MyClass(" << id << ") destroyed\n"; }int id;
};int main() {// 创建 shared_ptr (三种方式)auto sp1 = std::make_shared<MyClass>(1);       // 推荐:高效,异常安全std::shared_ptr<MyClass> sp2(new MyClass(2));  // 不推荐:二次分配auto sp3 = std::shared_ptr<MyClass>(sp2);      // 拷贝构造,引用计数变为2std::cout << "sp1 use count: " << sp1.use_count() << "\n"; // 1std::cout << "sp2 use count: " << sp2.use_count() << "\n"; // 2std::cout << "sp3 use count: " << sp3.use_count() << "\n"; // 2// 共享所有权{auto sp4 = sp2;  // 新的拷贝,引用计数变为3std::cout << "Inside scope, use count: " << sp4.use_count() << "\n"; // 3} // sp4 销毁,引用计数回到2// 重置指针sp2.reset();        // sp2 不再拥有对象,引用计数变为1std::cout << "After sp2 reset, sp3 use count: " << sp3.use_count() << "\n"; // 1return 0;
} // sp1, sp3 销毁,引用计数变为0,对象被销毁

输出结果:

MyClass(1) constructed
MyClass(2) constructed
sp1 use count: 1
sp2 use count: 2
sp3 use count: 2
Inside scope, use count: 3
After sp2 reset, sp3 use count: 1
MyClass(1) destroyed
MyClass(2) destroyed

3.2 自定义删除器

shared_ptr 支持自定义删除器,用于非默认的资源释放逻辑:

#include <cstdio>// 自定义文件删除器
struct FileDeleter {void operator()(FILE* fp) const {if (fp) {std::fclose(fp);std::cout << "File closed\n";}}
};int main() {// 使用自定义删除器管理文件指针std::shared_ptr<FILE> file_ptr(std::fopen("example.txt", "w"), FileDeleter());if (file_ptr) {std::fputs("Hello, shared_ptr!", file_ptr.get());}// 离开作用域时自动调用 FileDeleter 关闭文件return 0;
}

也可使用 lambda 表达式作为删除器:

auto deleter = [](MyClass* p) {std::cout << "Custom deleter for MyClass(" << p->id << ")\n";delete p;
};std::shared_ptr<MyClass> sp(new MyClass(3), deleter);

3.3 数组支持 (C++17)

C++17 开始支持管理动态数组:

// C++17 数组支持
auto arr_ptr = std::make_shared<int[]>(5); // 创建包含5个int的数组// 访问数组元素
for (int i = 0; i < 5; ++i) {arr_ptr[i] = i * 10;
}// 数组版本会自动使用 delete[] 释放资源

3.4 与 weak_ptr 配合使用

shared_ptr 的最大问题是可能产生循环引用,此时需要 weak_ptr 打破循环:

#include <memory>struct Node {int data;std::shared_ptr<Node> next;  // 共享指针导致循环引用// std::weak_ptr<Node> next;  // 使用弱指针打破循环
};int main() {auto node1 = std::make_shared<Node>(1);auto node2 = std::make_shared<Node>(2);node1->next = node2;node2->next = node1;  // 形成循环引用// 此时 node1 和 node2 的引用计数都是2,离开作用域后不会减为0// 导致内存泄漏!return 0;
}

解决方法是将其中一个 shared_ptr 改为 weak_ptr

struct Node {int data;std::weak_ptr<Node> next;  // 弱指针不增加引用计数
};

weak_ptr 特性:

  • 不拥有对象所有权,不增加引用计数
  • 可通过 lock() 方法获取 shared_ptr(如果对象存在)
  • 用于解决循环引用问题
  • 可通过 expired() 检查对象是否已被销毁

四、高级应用场景

4.1 多线程共享资源

shared_ptr 的引用计数操作是线程安全的,可以安全地在多线程间传递:

#include <thread>
#include <vector>void func(std::shared_ptr<MyClass> sp) {// 线程安全:引用计数的增减是原子操作std::this_thread::sleep_for(std::chrono::milliseconds(10));std::cout << "Thread " << std::this_thread::get_id() << ", use count: " << sp.use_count() << "\n";
}int main() {auto sp = std::make_shared<MyClass>(1);std::vector<std::thread> threads;// 创建5个线程共享 spfor (int i = 0; i < 5; ++i) {threads.emplace_back(func, sp);}for (auto& t : threads) {t.join();}std::cout << "Main thread, use count: " << sp.use_count() << "\n";return 0;
}

4.2 作为容器元素

shared_ptr 可安全存储在标准容器中:

#include <vector>
#include <algorithm>int main() {std::vector<std::shared_ptr<MyClass>> objects;// 添加元素objects.push_back(std::make_shared<MyClass>(1));objects.push_back(std::make_shared<MyClass>(2));objects.push_back(std::make_shared<MyClass>(3));// 遍历并访问元素std::for_each(objects.begin(), objects.end(), [](const auto& sp) {std::cout << "MyClass id: " << sp->id << "\n";});return 0;
}

4.3 类型转换

shared_ptr 提供专门的类型转换函数,类似于 C++ 的强制类型转换:

struct Base { virtual ~Base() = default; };
struct Derived : Base { int value = 42; };int main() {std::shared_ptr<Base> base_ptr = std::make_shared<Derived>();// 动态转换(安全检查)if (auto derived_ptr = std::dynamic_pointer_cast<Derived>(base_ptr)) {std::cout << "Derived value: " << derived_ptr->value << "\n";}// 静态转换(无安全检查)auto derived_ptr = std::static_pointer_cast<Derived>(base_ptr);std::cout << "Derived value: " << derived_ptr->value << "\n";return 0;
}

五、注意事项与最佳实践

5.1 避免的操作

  1. 不要将原始指针交给多个 shared_ptr

    // 错误示例:多个 shared_ptr 独立管理同一资源
    int* raw_ptr = new int(10);
    std::shared_ptr<int> sp1(raw_ptr);
    std::shared_ptr<int> sp2(raw_ptr); // 严重错误!两个独立控制块,导致双重释放
    
  2. 避免循环引用

    // 循环引用导致内存泄漏
    struct A { std::shared_ptr<B> b; };
    struct B { std::shared_ptr<A> a; };auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b = b;
    b->a = a; // 循环引用,引用计数永远不为0
    

    解决:将其中一个改为 std::weak_ptr

  3. 不要用 shared_ptr 管理栈上对象

    // 错误示例:管理栈上对象
    int x = 10;
    std::shared_ptr<int> sp(&x); // 析构时会调用 delete &x,导致未定义行为
    

5.2 最佳实践

  1. 优先使用 std::make_shared

    • 优点1:一次分配内存(控制块+对象),效率更高
    • 优点2:异常安全,避免资源泄漏
    auto sp1 = std::make_shared<MyClass>(); // 推荐
    auto sp2 = std::shared_ptr<MyClass>(new MyClass()); // 不推荐
    
  2. 避免使用 get() 返回的原始指针

    • 不要将 get() 返回的指针交给另一个智能指针
    • 不要手动释放 get() 返回的指针
  3. 正确处理数组(C++17前)

    • C++17 前 shared_ptr 不直接支持数组,需提供自定义删除器
    // C++17 前管理数组的正确方式
    std::shared_ptr<int> arr_ptr(new int[5], std::default_delete<int[]>());
    
  4. 使用 weak_ptr 解决循环引用

    • 在双向链表、树等数据结构中,父节点持有子节点的 shared_ptr,子节点持有父节点的 weak_ptr
  5. 线程安全注意事项

    • shared_ptr 本身的引用计数操作是线程安全的
    • 但管理的对象不是线程安全的,仍需同步机制保护

六、shared_ptr 与 unique_ptr 对比

特性shared_ptrunique_ptr
所有权共享所有权独占所有权
大小两个指针大小一个指针大小
引用计数
拷贝操作允许(增加计数)禁止
移动操作允许允许
循环引用可能(需 weak_ptr 解决)不可能
数组支持C++17 开始支持原生支持
性能引用计数操作有开销无额外开销

选择建议

  • 当需要共享资源时,使用 shared_ptr
  • 当资源仅需独占时,使用 unique_ptr(性能更优)
  • 优先考虑 unique_ptr,仅在必要时才使用 shared_ptr

七、总结

std::shared_ptr 是 C++ 中实现共享所有权的智能指针,通过引用计数机制允许多个指针共同管理同一资源。其核心特点包括:

  • 共享所有权:多个 shared_ptr 可指向同一对象
  • 自动释放:最后一个所有者销毁时释放资源
  • 线程安全:引用计数操作是原子的,支持多线程环境
  • 灵活性:支持自定义删除器和分配器
  • 配合 weak_ptr:可解决循环引用问题

在实际开发中,应根据资源所有权需求选择合适的智能指针,优先考虑 unique_ptr 以获得更好的性能,仅在需要共享资源时使用 shared_ptr

http://www.lryc.cn/news/579356.html

相关文章:

  • 算法分析的系统性总结
  • FFmpeg开发笔记(七十七)Android的开源音视频剪辑框架RxFFmpeg
  • Python 物联网(IoT)与边缘计算开发实战(1)
  • 基于多线程实现链表快排
  • 如何有效的开展接口自动化测试?
  • Linux之Socket 编程 UDP
  • C++ 项目实践:如何用对象池优化内存管理、解决 MISRA 报警
  • 制作一款打飞机游戏76:分数显示
  • CentOS系统高效部署fastGPT全攻略
  • Android音视频探索之旅 | CMake基础语法 创建支持Ffmpeg的Android项目
  • 电脑CPU使用率占用100%怎么办 解决步骤指南
  • 按键精灵 安卓脚本开发:游戏实战之自动切换账号辅助工具
  • 需要scl来指定编译器的clangd+cmake在vscode/cursor开发环境下的配置
  • reactnative页面适配UI设计尺寸px转dp的完美解决方案px2dp
  • 9.Docker的容器数据卷使用(挂载)
  • CAD2018,矩形设计,新增文字,块新增与打散
  • snail-job的oracle sql(oracle 11g)
  • OFD|WPS|PDF 文档在线预览-高级功能
  • 前置代理重构网络访问的「中转站」
  • AI大模型的技术演进、流程重构、行业影响三个维度的系统性分析
  • 嵌入式系统中实现串口重定向
  • DMN方式的特点
  • 《P2572 [SCOI2010] 序列操作》
  • maker-pdf 文档文字识别,并用python实现
  • 专题:2025即时零售与各类人群消费行为洞察报告|附400+份报告PDF、原数据表汇总下载
  • 2025年6月:技术探索与生活平衡的协奏曲
  • 从零开始构建Airbyte数据管道:PostgreSQL到BigQuery实战指南
  • 基于定制开发开源AI智能名片与S2B2C商城小程序的搜索区用户需求洞察与精准服务研究
  • WebRTC 安全性分析研究
  • C# 线程同步(一)同步概念介绍