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

C++|手写shared_ptr实现

参考:https://mp.weixin.qq.com/s/QIYuJQU-6cJC8kjk0pn5ew

文章目录

  • 🧠 C++|手写 shared_ptr 实现
    • 🩵 前言:为什么要手写 `shared_ptr`?
    • 🧩 Part 1. shared_ptr 的原理剖析
      • 1.1 所有权共享与引用计数
    • ⚙️ Part 2. 手写一个简易版 `SharedPtr`
      • 🧱 Step 1. 定义控制块(Control Block)
      • 🧪 Step 2. 测试我们自己的 `SharedPtr`
    • 🧠 Part 3. 实现更完整的功能
      • 3.1 支持移动语义
    • 一、移动构造函数 & 移动赋值运算符
      • 🔹1. 什么是“移动语义”?
      • 🔹2. 移动构造函数
      • 🔹3. 移动赋值运算符
    • 二、`noexcept` 的作用
    • 三、`reset()` 与 `unique()`
      • 🔹`reset()`
      • 🔹`unique()`
    • ✅ 总结
    • ⚡ Part 4. 循环引用问题
    • 🧵 Part 5. 线程安全性简析
  • 🧩 Part 6. 完整源码 + 引用计数生命周期图
    • 🧱 6.1 完整版 SharedPtr 实现
    • 🧮 6.2 引用计数生命周期图
    • 💡 6.3 关键点回顾
    • 🧭 总结图示(整篇核心逻辑一图懂)
    • ✅ 6.4 总结一句话
  • 🏁 总结
    • 💡 后记


🧠 C++|手写 shared_ptr 实现

从理解智能指针的原理到动手实现一个属于你自己的 shared_ptr。


🩵 前言:为什么要手写 shared_ptr

在 C++ 的世界里,内存管理是一门“艺术”。
手动 new / delete 往往会带来三大噩梦:

  • ❌ 内存泄漏(忘记释放)
  • ❌ 野指针(提前释放)
  • ❌ 重复释放(多次 delete)

为了解决这些问题,C++11 引入了 智能指针(Smart Pointer)
其中最具代表性的就是 —— std::shared_ptr

shared_ptr 的核心思想是:

多个智能指针可以“共享”同一个对象的所有权,当最后一个指针消失时,对象才会被释放。


🧩 Part 1. shared_ptr 的原理剖析

1.1 所有权共享与引用计数

shared_ptr 内部维护着两个关键数据:

  1. 原始指针(raw pointer) → 指向被管理的对象
  2. 引用计数(ref_count) → 记录有多少个 shared_ptr 指向这块内存

当你复制一个 shared_ptr 时,计数 +1;
当一个 shared_ptr 析构时,计数 -1;
当计数为 0 时,对象自动 delete

可以简单理解为👇:

         ┌──────────────┐
ptr1 ───▶│   Object     │├──────────────┤
ptr2 ───▶│ RefCount=2   │└──────────────┘

⚙️ Part 2. 手写一个简易版 SharedPtr

我们从最小可行版本开始实现。

🧱 Step 1. 定义控制块(Control Block)

控制块负责保存:

  • 引用计数
  • 原始指针
template <typename T>
class SharedPtr {
private:T* ptr;              // 被管理的对象size_t* ref_count;   // 引用计数(动态分配)public:// 构造函数explicit SharedPtr(T* p = nullptr): ptr(p), ref_count(new size_t(1)) {}// 拷贝构造函数SharedPtr(const SharedPtr& other): ptr(other.ptr), ref_count(other.ref_count) {++(*ref_count);}// 拷贝赋值函数SharedPtr& operator=(const SharedPtr& other) {if (this != &other) {release(); // 释放原有对象ptr = other.ptr;ref_count = other.ref_count;++(*ref_count);}return *this;}// 析构函数~SharedPtr() {release();}void release() {if (--(*ref_count) == 0) {delete ptr;delete ref_count;}}T& operator*() const { return *ptr; }T* operator->() const { return ptr; }size_t use_count() const { return *ref_count; }
};

🧪 Step 2. 测试我们自己的 SharedPtr

#include <iostream>struct Test {Test() { std::cout << "Constructed\n"; }~Test() { std::cout << "Destructed\n"; }void hello() { std::cout << "Hello from Test!\n"; }
};int main() {SharedPtr<Test> sp1(new Test());std::cout << "use_count: " << sp1.use_count() << "\n";{SharedPtr<Test> sp2 = sp1; // 拷贝,共享所有权std::cout << "use_count: " << sp1.use_count() << "\n";sp2->hello();} // sp2 离开作用域,计数 -1std::cout << "use_count after scope: " << sp1.use_count() << "\n";return 0;
}

🖥 输出示例:

Constructed
use_count: 1
use_count: 2
Hello from Test!
use_count after scope: 1
Destructed

我们自己的 shared_ptr 成功自动管理了内存!


🧠 Part 3. 实现更完整的功能

3.1 支持移动语义

非常好,这里你问的几个概念正是理解智能指针(比如 SharedPtrunique_ptr)的关键点。我们分几部分讲清楚:


一、移动构造函数 & 移动赋值运算符

🔹1. 什么是“移动语义”?

传统上,在 C++98 中,对象之间的赋值或传参只能通过“拷贝”(复制)完成:

  • 拷贝构造函数(T(const T&)
  • 拷贝赋值运算符(T& operator=(const T&)

但拷贝意味着:

  • 要复制所有资源(比如动态内存、文件句柄等),
  • 可能非常低效,甚至不安全(两个对象同时释放同一块内存)。

C++11 引入了“移动语义(move semantics)”,允许对象的资源“转移所有权”,而不是复制。
这依赖两个新特性:

  • 右值引用(T&&
  • 移动构造函数 / 移动赋值运算符

🔹2. 移动构造函数

SharedPtr(SharedPtr&& other) noexcept: ptr(other.ptr), ref_count(other.ref_count) {other.ptr = nullptr;other.ref_count = nullptr;
}
  • SharedPtr&& other 表示“右值引用”,即可以绑定到临时对象。

  • 移动构造函数的目的:直接接管另一个对象的资源,而不是拷贝

  • 它会:

    • 把资源(ptrref_count)从 other 移到当前对象;
    • other 置为空(防止它析构时释放同一资源)。

例如:

SharedPtr<int> a(new int(10));
SharedPtr<int> b(std::move(a));  // 调用移动构造

执行完后:

  • b 拥有那块内存;
  • a 变成空的(a.ptr == nullptr)。

🔹3. 移动赋值运算符

SharedPtr& operator=(SharedPtr&& other) noexcept {if (this != &other) {release();  // 先释放当前对象持有的资源ptr = other.ptr;ref_count = other.ref_count;other.ptr = nullptr;other.ref_count = nullptr;}return *this;
}

作用与移动构造类似,只是发生在已存在的对象之间的赋值中。
比如:

SharedPtr<int> a(new int(10));
SharedPtr<int> b;
b = std::move(a);  // 调用移动赋值

执行完:

  • b 拥有资源;
  • a 被清空。

二、noexcept 的作用

noexcept 关键字表示“这个函数承诺不会抛出异常”。

为什么重要?

  • C++ 标准库在优化时会优先选择 noexcept 的移动操作
    例如 std::vector 在扩容移动元素时:

    • 如果移动构造是 noexcept 的,就用移动;
    • 否则退回用拷贝(因为拷贝可保证异常安全)。

所以:

SharedPtr(SharedPtr&& other) noexcept

告诉编译器:

“放心,这个移动操作不会抛异常,可以安全地优化使用移动语义。”


三、reset()unique()

🔹reset()

void reset(T* p = nullptr) {release();              // 释放当前对象所占有的资源ptr = p;                // 接管新的原始指针ref_count = new size_t(1); // 初始化引用计数
}

作用:

  • 把当前智能指针“重置”为管理新的资源;
  • 如果不传参数,相当于置空(释放旧资源)。

例:

SharedPtr<int> sp(new int(10));
sp.reset(new int(20));  // 释放旧的 10,管理新的 20
sp.reset();             // 释放资源,ptr 变为空

🔹unique()

bool unique() const { return *ref_count == 1; }

作用:
判断当前指针是否是资源的唯一持有者

例:

SharedPtr<int> a(new int(5));
SharedPtr<int> b = a;
std::cout << a.unique(); // false,因为 a 和 b 共享同一资源
b.reset();
std::cout << a.unique(); // true,此时只有 a 在用

✅ 总结

名称功能关键点
移动构造 T(T&&)从临时对象中“偷取”资源不拷贝,仅转移所有权
移动赋值 T& operator=(T&&)把已有对象的资源替换为另一个临时对象的资源注意释放旧资源
noexcept声明函数不会抛异常使 STL 优化使用移动语义
reset()释放旧资源,接管新资源可选参数
unique()判断是否唯一持有资源引用计数 == 1

⚡ Part 4. 循环引用问题

虽然 shared_ptr 非常强大,但也有一个致命陷阱:循环引用。

示例:

struct B;struct A {std::shared_ptr<B> bptr;~A() { std::cout << "A destroyed\n"; }
};struct B {std::shared_ptr<A> aptr;~B() { std::cout << "B destroyed\n"; }
};int main() {auto a = std::make_shared<A>();auto b = std::make_shared<B>();a->bptr = b;b->aptr = a; // 循环引用,永远不会释放!
}

输出:

(没有任何析构输出)

🧩 原因:
ab 的引用计数永远大于 0,析构函数不会被调用。

解决方案: 使用 std::weak_ptr 打破循环:

struct B;
struct A {std::shared_ptr<B> bptr;~A() { std::cout << "A destroyed\n"; }
};
struct B {std::weak_ptr<A> aptr;  // 弱引用~B() { std::cout << "B destroyed\n"; }
};

🧵 Part 5. 线程安全性简析

  • 引用计数递增/递减是线程安全的(原子操作)
  • ⚠️ 对象本身的访问不是线程安全的,需要 mutex 保护

例如:

std::mutex mtx;
auto counter = std::make_shared<int>(0);std::thread t1([&]{for(int i=0;i<100;++i) {std::lock_guard<std::mutex> lock(mtx);++(*counter);}
});

🧩 Part 6. 完整源码 + 引用计数生命周期图

🧱 6.1 完整版 SharedPtr 实现

下面的代码是一个可独立运行的完整版本,包含:
✅ 拷贝 / 移动构造与赋值
✅ reset / unique / use_count
✅ 自动释放机制

#include <iostream>
#include <utility> // for std::exchangetemplate <typename T>
class SharedPtr {
private:T* ptr;               // 被管理的对象size_t* ref_count;    // 引用计数void release() {if (ref_count) {if (--(*ref_count) == 0) {delete ptr;delete ref_count;std::cout << "[SharedPtr] Object deleted.\n";}}}public:// 构造函数explicit SharedPtr(T* p = nullptr): ptr(p), ref_count(p ? new size_t(1) : nullptr) {}// 拷贝构造函数SharedPtr(const SharedPtr& other): ptr(other.ptr), ref_count(other.ref_count) {if (ref_count) ++(*ref_count);}// 移动构造函数SharedPtr(SharedPtr&& other) noexcept: ptr(std::exchange(other.ptr, nullptr)),ref_count(std::exchange(other.ref_count, nullptr)) {}// 拷贝赋值SharedPtr& operator=(const SharedPtr& other) {if (this != &other) {release();ptr = other.ptr;ref_count = other.ref_count;if (ref_count) ++(*ref_count);}return *this;}// 移动赋值SharedPtr& operator=(SharedPtr&& other) noexcept {if (this != &other) {release();ptr = std::exchange(other.ptr, nullptr);ref_count = std::exchange(other.ref_count, nullptr);}return *this;}// 析构函数~SharedPtr() {release();}// 访问T& operator*() const { return *ptr; }T* operator->() const { return ptr; }// 实用接口size_t use_count() const { return ref_count ? *ref_count : 0; }bool unique() const { return use_count() == 1; }void reset(T* p = nullptr) {release();if (p) {ptr = p;ref_count = new size_t(1);} else {ptr = nullptr;ref_count = nullptr;}}
};// 测试用例
struct Test {Test(int id) : id(id) { std::cout << "Test " << id << " constructed\n"; }~Test() { std::cout << "Test " << id << " destructed\n"; }void hello() { std::cout << "Hello from Test " << id << "\n"; }int id;
};int main() {SharedPtr<Test> p1(new Test(1));std::cout << "use_count: " << p1.use_count() << "\n";{SharedPtr<Test> p2 = p1;std::cout << "use_count: " << p1.use_count() << "\n";p2->hello();}std::cout << "use_count after scope: " << p1.use_count() << "\n";p1.reset();
}

🖥️ 运行输出示例:

Test 1 constructed
use_count: 1
use_count: 2
Hello from Test 1
use_count after scope: 1
Test 1 destructed
[SharedPtr] Object deleted.

🧮 6.2 引用计数生命周期图

下面是一个简单的生命周期流程图(可以插图或用 Mermaid 渲染)👇

graph TDA[构造 SharedPtr p1] -->|ref_count=1| B[复制 SharedPtr p2 = p1]B -->|ref_count=2| C[作用域结束,p2 析构]C -->|ref_count=1| D[p1 仍持有对象]D -->|p1.reset() 或析构| E[ref_count=0,对象释放]E --> F[delete 对象内存]

🧩 说明:

  • 每次拷贝:引用计数 +1
  • 每次销毁:引用计数 -1
  • 当计数归 0 → 自动释放内存

💡 6.3 关键点回顾

功能点机制说明
构造 / 拷贝 / 析构控制引用计数的创建、递增、递减
release()负责安全释放资源
reset()主动更换管理对象
unique()判断是否唯一拥有者
use_count()获取当前引用数量

🧭 总结图示(整篇核心逻辑一图懂)

 ┌──────────────┐│  new Object  │└──────┬───────┘│▼┌──────────────────────┐│ SharedPtr            ││  ptr ─────────────┐  ││  ref_count = 1     │  │└────────────────────┘  │▲                ││ 拷贝           │┌──────────────────────┘│ SharedPtr(copy)       ││  ptr (同一对象)       ││  ref_count = 2        │└──────────────────────┘│▼析构一个 → ref_count=1全部析构 → ref_count=0 → delete

✅ 6.4 总结一句话

shared_ptr 的核心就是 “引用计数控制共享对象的生命周期”。

手写一遍之后,你会更深刻地理解 C++ RAII 的精神:

资源的获取即初始化,销毁即释放。


是否希望我帮你把这篇博客加上 封面标题图 + Mermaid 渲染图的 Markdown 版排版模板
(我可以给你一个直接复制到公众号 / Typora / Notion 都能渲染的版本。)

🏁 总结

特性说明
✅ 自动内存释放引用计数归零时自动析构
✅ 支持共享所有权多个智能指针共享同一资源
⚠️ 存在循环引用风险需使用 weak_ptr 打破
⚠️ 对象访问非线程安全需配合 mutex 使用

💡 后记

手写一个 shared_ptr 并不是为了取代标准库的实现,而是为了彻底理解其核心原理

“智能指针的本质,是对资源生命周期的自动化管理。”

理解了这一点,C++ 的 RAII 思想将贯穿你写的每一行代码。


📚 延伸阅读

  • 《Effective Modern C++》— Item 19:用 std::shared_ptr 管理共享资源
  • 《Inside the C++ Object Model》— 智能指针的对象语义与资源语义

太好了 👍
下面补上完整的 Part 6:完整源码 + 引用计数生命周期图,这部分可以直接附在你博客文末作为“源码与可视化总结”部分。


http://www.dtcms.com/a/498918.html

相关文章:

  • Python 数据分析入门:Pandas vs NumPy 全方位对比
  • 学做网站能赚钱吗信息平台网站模板
  • Redis为什么是单线程的
  • layuiadmin与laravel 12 前后端分离nginx配置
  • IDEA在文件中查找快捷键失效
  • 整合知识图谱与大语言模型:下一代药物发现的革命性技术
  • 详解TCP(详细版)
  • TCMalloc原理解析(上)
  • OpenCV深度学习:目标检测、人脸识别与智能视频分
  • 电子商务网站有哪些内容wordpress漏洞2019
  • FileNotFoundError: [WinError 3] 系统找不到指定的路径。
  • qq音乐怎么做mp3下载网站安阳县教育局官网
  • 深度学习进阶(七)——智能体的进化:从 LLM 到 AutoGPT 与 OpenDevin
  • Redis面试八股
  • 做网站从设计到上线流程山西专业网站建设价目
  • 排序算法:详解插入排序
  • [MLflow] 部署OpenAI聊天网关 | 令牌桶算法限流 | 分布式追踪 | Trace Span
  • 【010】智能图书系统
  • LeetCode 刷题【124. 二叉树中的最大路径和】
  • linux 的文件结构
  • stack,queue,咕咕咕!
  • 做网站不实名认证可以吗佛山城市建设工程有限公司
  • 构建基于大语言模型的智能数据可视化分析工具的学习总结
  • Android 架构演进全解析:MVC、MVP、MVVM、MVI 图文详解
  • 网站后台免费模板下载艺术字体在线生成器转换器
  • HC32 操作GPIO点亮LED(HC库)
  • 如何用python来做小游戏
  • 捡到h3开发板,做了个视频小车(二),御游追风plus做遥控器
  • U盘安装群晖RR引导
  • 昆山市住房和城乡建设网站wordpress淘宝联盟