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
内部维护着两个关键数据:
- 原始指针(raw pointer) → 指向被管理的对象
- 引用计数(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 支持移动语义
非常好,这里你问的几个概念正是理解智能指针(比如 SharedPtr
或 unique_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
表示“右值引用”,即可以绑定到临时对象。 -
移动构造函数的目的:直接接管另一个对象的资源,而不是拷贝。
-
它会:
- 把资源(
ptr
和ref_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; // 循环引用,永远不会释放!
}
输出:
(没有任何析构输出)
🧩 原因:
a
和 b
的引用计数永远大于 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:完整源码 + 引用计数生命周期图,这部分可以直接附在你博客文末作为“源码与可视化总结”部分。