尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
仿现代C++智能指针实现引用计数
前言std::shared_ptr的核心是「共享所有权 引用计数」多个智能指针指向同一个对象计数记录有多少个持有者最后一个持有者销毁时释放对象。自己实现一个简化版能把控制块、计数增减、移动语义和异常安全这几件事串起来也能看清标准库为什么需要weak_ptr。常见的误解是「引用计数是线程安全的所以 shared_ptr 指向的对象也线程安全」。标准保证的是控制块的计数操作是原子的多个线程可以各自复制、销毁不同的shared_ptr实例但对同一个对象的读写没有任何自动同步那仍然是数据竞争属于未定义行为undefined behavior, UB。另一个误解是「用同一个裸指针构造两个 shared_ptr 没问题」。每个shared_ptr会各自创建一个控制块两个控制块都认为自己是唯一持有者析构时对同一指针释放两次直接重复释放。本文实现一个只含强引用计数的SharedPtr说明控制块的结构、拷贝/移动/析构的计数平衡以及它和标准库shared_ptr的差距。一、控制块与计数规则控制块control block保存三样东西被管理对象的指针、删除器、强引用计数。所有共享同一对象的SharedPtr指向同一个控制块。操作计数变化用裸指针构造新建控制块计数为 1拷贝构造计数加一移动构造计数不变源指针置空拷贝赋值先增加新值的计数再减少旧值的计数析构计数减一减到 0 时删除对象和控制块拷贝赋值用「拷贝并交换」实现时参数按值传递已经完成了计数增加交换后临时对象析构完成计数减少逻辑自然平衡也天然处理自赋值。删除器用函数指针保存类型擦除后控制块不必是模板。对T的删除动作是delete static_castT*(raw)如果对象是new[]分配的必须用delete[]本文的简化版不支持数组使用数组类型会出错。二、拷贝、移动与释放拷贝构造只做两件事复制指针、计数加一。它不分配新对象因此多个SharedPtr共享同一份数据。移动构造把指针和控制块「偷」过来并把源置空。移动后源对象处于有效但未指定状态本文的实现把它变成空指针析构时不会减少计数。被移动的对象只能销毁或重新赋值不要解引用。析构调用release()若控制块存在计数减一若减之前是 1说明当前是最后一个持有者调用删除器释放对象再删除控制块。这里必须用「减一后的返回值」判断不能先读计数再减否则多线程下会出错。三、完整实现// 适用于 C17#include atomic#include cstddef#include utilityclass ControlBlock {public:ControlBlock(void* target, void (*deleter)(void*)): ptr(target), destroy(deleter) {}std::atomicstd::size_t refs{1};void* ptr;void (*destroy)(void*);};template class Tclass SharedPtr {public:SharedPtr() noexcept default;explicit SharedPtr(T* target) : ptr_(target) {if (ptr_ ! nullptr) {try {cb_ new ControlBlock(ptr_, [](void* raw) { delete static_castT*(raw); });} catch (...) {delete ptr_; // 控制块分配失败释放已创建的对象ptr_ nullptr;throw;}}}SharedPtr(const SharedPtr other) noexcept: ptr_(other.ptr_), cb_(other.cb_) {if (cb_ ! nullptr) {cb_-refs.fetch_add(1, std::memory_order_relaxed);}}SharedPtr(SharedPtr other) noexcept: ptr_(other.ptr_), cb_(other.cb_) {other.ptr_ nullptr;other.cb_ nullptr;}SharedPtr operator(SharedPtr other) noexcept {swap(other);return *this;}~SharedPtr() {release();}void swap(SharedPtr other) noexcept {std::swap(ptr_, other.ptr_);std::swap(cb_, other.cb_);}void reset() noexcept {SharedPtr empty;swap(empty);}T* get() const noexcept { return ptr_; }T operator*() const noexcept { return *ptr_; }T* operator-() const noexcept { return ptr_; }explicit operator bool() const noexcept { return ptr_ ! nullptr; }std::size_t useCount() const noexcept {return cb_ nullptr ? 0 : cb_-refs.load(std::memory_order_relaxed);}private:void release() noexcept {if (cb_ nullptr) {return;}if (cb_-refs.fetch_sub(1, std::memory_order_acq_rel) 1) {cb_-destroy(cb_-ptr);delete cb_;}ptr_ nullptr;cb_ nullptr;}T* ptr_ nullptr;ControlBlock* cb_ nullptr;};template class T, class... ArgsSharedPtrT makeShared(Args... args) {return SharedPtrT(new T(std::forwardArgs(args)...));}使用示例// 适用于 C17#include iostream#include stringint main() {SharedPtrstd::string first(new std::string(hello));{SharedPtrstd::string second first; // 计数为 2std::cout second.useCount() \n;second-append( world);} // second 析构计数回到 1std::cout *first \n;std::cout first.useCount() \n;return 0;}按标准语义第一个输出是 2second离开作用域后计数回到 1first仍指向被修改过的字符串。这里描述的是引用计数规则不是某次运行的输出。几个实现细节release()用fetch_sub的返回值判断是否归零memory_order_acq_rel保证最后一个持有者能看到其他线程在此之前的写入。若不确定内存序用默认的seq_cst更保守本文的增量用relaxed因为只是增加计数不涉及对象访问的同步。operator按值接收拷贝或移动在参数构造阶段完成交换后临时对象析构释放旧值。自赋值时先加一后减一安全。reset()通过和一个空对象交换实现旧值随后析构。makeShared先new T再构造SharedPtr如果SharedPtr构造中控制块分配失败构造函数会删除对象不会泄漏。标准库的make_shared通常把对象和控制块放在一次分配里效率更高本文从简。四、与标准库的差距特性本文 SharedPtrstd::shared_ptr强引用计数有有弱引用weak_ptr无有循环引用无法打破用weak_ptr打破数组支持无delete而非delete[]C17 起可管理数组自定义删除器仅函数指针任意可调用对象别名构造无有控制块分配与对象分开make_shared可合并线程安全计数原子对象不保证同样没有弱引用就无法解决循环引用两个对象互相持有SharedPtr计数永远不会归零对象泄漏。标准库的weak_ptr通过「弱计数」观察控制块不增加强计数最后一个强引用释放对象最后一个弱引用释放控制块。常见坑点❌ 用同一个裸指针构造两个SharedPtr。// 适用于 C17int* raw new int(1);SharedPtrint a(raw);SharedPtrint b(raw); // 两个控制块重复释放✅ 只用一个裸指针构造一次其余用拷贝。// 适用于 C17SharedPtrint a(new int(1));SharedPtrint b a;❌ 认为计数线程安全等于对象线程安全。// 适用于 C17SharedPtrCounter p makeSharedCounter();// 两个线程同时 p-increment()仍是数据竞争✅ 对共享对象的访问另行加锁或用原子成员。// 适用于 C17std::mutex m;std::lock_guardstd::mutex lock(m);p-increment();❌ 移动后继续解引用源对象。// 适用于 C17SharedPtrint b std::move(a);std::cout *a; // a 已为空解引用空指针UB✅ 移动后只销毁或重新赋值源对象。// 适用于 C17SharedPtrint b std::move(a);a SharedPtrint(new int(0));❌ 用delete释放new[]的数组。// 适用于 C17SharedPtrint p(new int[10]); // 删除器用 deleteUB✅ 使用std::shared_ptrint[]C17或自定义delete[]删除器。// 适用于 C17std::shared_ptrint[] p(new int[10]);❌ 循环引用。// 适用于 C17struct Node {SharedPtrNode next;};// a-next b; b-next a; 计数永不归零✅ 其中一个方向改用弱引用。// 适用于 C17std::weak_ptrNode next;❌ 在构造函数里把this交给SharedPtr。// 适用于 C17class X {public:X() { SharedPtrX self(this); } // 又一个控制块重复释放};✅ 继承std::enable_shared_from_this用shared_from_this()。// 适用于 C17class X : public std::enable_shared_from_thisX {};❌ 控制块分配失败时泄漏对象。// 适用于 C17explicit SharedPtr(T* p) : ptr_(p) {cb_ new ControlBlock(...); // 抛异常ptr_ 泄漏}✅ 捕获异常并释放对象。// 适用于 C17try { cb_ new ControlBlock(...); }catch (...) { delete ptr_; throw; }❌ 用use_count()做业务判断。// 适用于 C17if (p.useCount() 1) { /* 认为独占直接改内部状态 */ }✅ 计数只是观察值多线程下可能立刻变化独占语义应用unique()并配合同步设计。总结要点做法控制块保存指针、删除器、原子强计数拷贝计数加一移动转移指针源置空析构计数减一归零时删除对象和控制块赋值拷贝并交换自赋值安全线程安全计数原子对象访问需另行同步循环引用需要弱引用本文未实现引用计数的代码不长正确性取决于两点每次拷贝和析构都严格配对地增减计数以及计数归零时只释放一次。把这两点守住再补上弱引用和自定义删除器就接近一个可用的共享指针了。
RELATED

相关推荐

斐波那契数列:从数学到计算机科学的奇妙应用

斐波那契数列:从数学到计算机科学的奇妙应用

1. 斐波那契数列的奇妙世界 第一次接触斐波那契数列是在大学数学课上,教授在黑板上写下那串神奇的数字:0,1,1,2,3,5,8,13...当时只觉得这是个有趣的数学游戏。直到后来在自然界中不断发现它的身影——向日葵的螺旋排列、松果的鳞片分布、鹦鹉螺的生长曲线…

📅 2026/10/26 11:46:21
什么是impeccable?从技术语义到工程实践中的完美性追求

什么是impeccable?从技术语义到工程实践中的完美性追求

项目标题为“impeccable”,但输入中未提供任何实质性项目正文、关键词列表或摘要描述,仅附有空置的网络搜索内容块( ),且无领域指向、技术线索、应用场景或上下文信息。 根据你设定的 核心创作原则第一条&#xff…

📅 2026/10/26 11:46:21
Linux I2C驱动开发:五大结构体、传输接口与排错实战

Linux I2C驱动开发:五大结构体、传输接口与排错实战

写 linux i2c 驱动,真正卡住新手的往往不是总线时序,而是那几个结构体到底谁管谁,以及接口的返回值到底代表什么。上一篇把 i2c 子系统的分层捋了一遍,这一篇专门啃结构体与接口:i2c_adapter、i2c_algorithm、i2c_clie…

📅 2026/10/26 11:46:21
MORE NEWS

更多资讯

📰

实测Claude中文能力跃升:从翻译腔到地道中文的技术解析

最近圈子里聊得最多的一个话题,就是Claude突然就会说中文了。这个"突然"当然不是毫无来由,但对普通用户来说,体感确实很直接:以前拿它处理中文内容,总带着一股"翻译腔",语法挑不出毛病…

📰

如何对LLM大型语言模型进行评估与基准测试:从性能指标到推理能力的落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

DeepSeek-R1模型选型与提示语工程实践指南

简介:这份PDF文档围绕国产开源通用人工智能推理模型DeepSeek展开,面向AI研发工程师、学者及希望提升效率的个人开发者,帮助读者建立从基础认知到进阶应用的完整路径。内容从DeepSeek与开源推理模型DeepSeek-R1的定位切入,梳理智能…

📰

AI赋能知识产权运营:四类智能体破解科技成果转化供需匹配难题

简介:围绕AI赋能的精准知识产权运营供需匹配平台,这份Word文档面向高校科研机构、技术转移从业者、企业创新负责人及政府科技管理部门,聚焦成果转化中信息不对称、资源分散、合作不畅等痛点。全文以单份docx交付,压缩包约38KB&…

📰

UG/NX二次开发实战弹药库:从环境搭建到UF函数调试

简介:本资源是面向UG(Unigraphics)初学者与进阶开发者的系统性二次开发学习合集,聚焦C API与GRIP脚本双路径实践,助力工程师在航空航天、汽车制造等工业领域实现定制化建模、自动化任务与界面扩展。压缩包共620个文件&…

📰

STP、RSTP、MSTP演进与实战:从环路告警到负载分担

1. 从一次环路告警说起:为什么需要MSTP很多刚接触交换网络的朋友,第一次看到机房里所有交换机的指示灯同时疯狂闪烁、CPU占用率飙到百分之百、业务大面积中断的场景,大概率是遇到了二层环路。我印象很深的一次,某办公区临时加了一…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬