C++手写shared_ptr共享智能指针|原子引用计数、强弱引用控制块、赋值重载底层深度剖析

📅 2026/7/31 4:09:52 👁️ 阅读次数 📝 编程学习
C++手写shared_ptr共享智能指针|原子引用计数、强弱引用控制块、赋值重载底层深度剖析

本篇博客自底向上拆解C++shared_ptr共享智能指针核心底层原理。

一、前置头文件配置

#define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; #include<cstdlib> #include<assert.h> #include<atomic>

知识点解析

  • atomic头文件:提供原子类型,用于实现线程安全的引用计数,解决多线程竞争问题,是shared_ptr支持多线程的核心。

  • _CRT_SECURE_NO_WARNINGS:屏蔽VS安全警告,适配本地编译运行。

  • 其余头文件用于常规IO、断言、内存操作,支撑整体代码运行。

二、自定义Int实体测试类 资源载体

//------------------------------------------------------- class Int { private: int value; public: Int(int x = 0) :value(x) { cout << "create Int Object" << this << " " << value << endl; } ~Int() { cout << "delete Int Object" << this << " " << value << endl; value = -1; } Int(const Int& it) :value(it.value) { cout << &it << "copy Int Object" << this << " " << value << endl; } Int(Int&& it) :value(it.value) { it.value = -1; cout << "move Int Object" << this << " " << value << endl; } Int operator=(Int&& it) { if (this != &it) { this->value = it.value; it.value = -1; } } Int& operator=(const Int& a) { if (this != &a) this->value = a.value; cout << this << "operator = (const Int &it)" << &a << endl; return *this; } void Print()const { cout << value << endl; } ostream& operator<< (ostream& os) const { os << value; return os; } istream& operator>> (istream& in) { in >> this->value; return in; } void Set(int x) { value = x; } int GetValue()const { return value; } int& Value() { return value; } const int& Value()const { return value; } Int& operator++() { this->value += 1; return *this;// } Int operator++(int) { Int tmp(*this); ++*this; return tmp; } operator int() const { return this->value; } Int* operator& () { return this; } const Int* operator& () const { return this; } };

知识点解析

  • 该类作为智能指针的测试资源载体,完整实现类的核心生命周期函数与运算符重载,用于观测内存创建、拷贝、移动、销毁全过程。

  • 生命周期函数:构造、析构、拷贝构造、移动构造、拷贝赋值、移动赋值,全程打印日志,直观观察智能指针资源管理逻辑。

  • 功能重载:输入输出运算符、前置/后置++、int隐式类型转换、取地址运算符,完善类的读写与运算能力。

  • 提供读写接口:Set/GetValue/Value(),兼顾普通对象与常量对象的取值安全。

  • 移动语义特性:资源转移后将原对象数值置-1,标记资源转移完成,避免资源混乱。

三、通用仿函数删除器 单对象数组适配

//------------------------------------------------------- //共享指针: // 一个堆区资源被多个指针所指向就叫做共享指针。 //对于移动语义,只有对堆区资源有意义。 //基于共享所有权,使用引用计数控制块管理资源对象的生命期。 //在容器中保存shared_ptr对象是安全的 template<class _Ty> struct default_deleter { void operator() (_Ty* _Ptr) const { delete _Ptr; } }; //------------------------------------------------------------------------------------------------- template<class _Ty> struct default_deleter<_Ty[]> { void operator() (_Ty* _Ptr) const { delete[]_Ptr; } };

知识点解析

  • 共享指针核心定义:一个堆区资源被多个指针指向,即为共享指针;移动语义仅对堆区资源有效,栈对象无意义。

  • shared_ptr核心原理:基于共享所有权,通过引用计数控制块统一管理资源生命周期,可安全存储在容器中。

  • 删除器为一元仿函数,通过模板特化适配两种内存释放规则:普通对象使用delete,数组对象使用delete[],杜绝内存释放出错。

  • 解耦设计:将内存释放逻辑与智能指针主体分离,灵活性更高,支持自定义删除器扩展。

四、强弱引用计数控制块 My_ReCount 线程安全核心

template<class _Ty> class My_ReCount { public: //typedef _Ty elemt_type; using elemt_ref = _Ty; //typedef _Ty* elemt_ptr; using pointer = _Ty*; private: _Ty* _Ptr; std::atomic<int> _Uses; //shared_ptr,针对于共享引用的引用计数 std::atomic<int> _Weaks; //weak_ptr,针对于弱引用的引用计数 /*定义了两个原子计数器,用于实现C++智能指针中的引用计数机制*/ public: My_ReCount(pointer& ps=nullptr) :_Ptr(ps), _Uses(0), _Weaks(0) { if (nullptr != ps)//如果没有地址,我们不能指向它,所以我们要去进行基本的判断。 { _Uses = 1; } ~My_ReCount() = default;//就并不是删除,删除是delete void Incref() { void Incwef() { _Weaks += 1; } --_Uses == 0) { Decwref(); } return --_Weaks; } int getwref() c int _use_count()const { return _Uses.load(一个原子操作,属于atomic头文件,它安全地获取 _Uses 的当前值,确保在多线程环境下获取到的值是一致的、未被修改的 } /*对于std::atomic<int> _Uses和std::atomi两个原子计数器,它们的操作(如 += 1或--)都是原子的,这意味着在多线程环境下,这些操作不会被其他线程中断或干扰。 具体来说: 完整执行,不会被其他线程打断 _Weaks += 1时,这个操作也会完整执行,不会被其他线程打断 这两个操作可以同时进不会互相干扰 原子操作保证了在多线程环境下引用计数的正确性,避免了竞争条件(race conditio的智能指针(如std::shared_ptr)的关键机制之一。*/ };n)的问题。这是实现线程安全行,但它们各自都是原子操作, 同样,当另一个线程执行 当一个线程执行_Uses += 1时,这个操作会c<int> _Weaks这); //_Uses.load() 是onst { return _Weaks; } return _Uses; } int Decwref() { int Decref() { if ( _Uses += 1; }是什么都不做的析构函数,这里 _Weaks = 1; //后面解释,与循环引用有关系 }

知识点解析

  • 核心成员:维护两个原子计数器_Uses管理shared_ptr强引用计数,_Weaks管理weak_ptr弱引用计数,天然支持多线程安全。

  • 原子操作特性:atomic类型的自增、自减、load操作都是原子性的,不会被线程打断,彻底解决多线程竞争条件问题。

  • 构造逻辑:传入有效堆地址时,默认将强弱引用计数都初始化为1,为后续解决循环引用问题做铺垫。

  • 析构函数默认空:控制块的销毁不由自身析构处理,由上层shared_ptr统一判断释放,职责分离。

  • 核心方法:

    • Incref():强引用计数+1,拷贝共享指针时调用;

    • Incwef():弱引用计数+1,适配弱指针场景;

    • Decref():强引用计数-1,计数归0时自动递减弱引用;

    • _use_count():原子读取当前强引用数量,保证读取数据准确。

五、核心手写 my_shared_ptr 完整实现

template<class _Ty, class _Dx = default_deleter<_Ty>> class my_shared_ptr { public: using elemtype = _Ty; using Deleter_type = _Dx; using pointer = _Ty*; private: _Ty* _mPtr;//heap;sys; My_ReCount<_Ty>* _mRep;//引用计数器 //面试问法?为什么不是用值,而用对象? //首先,如果是用值的计数器的话,很难保证时效性,也就是技术块是各自的,在假设已经有很多智能指针指向同一块内存空间的时候,当其中一个智能指针不指向这块内存中时,剩余的对象 //都要进行计数器的修改,此时系统的开销是十分巨大的,如果采用堆区的指针,就可以公用一个计数器,多个共享指针公用同一个计数器,保证了时效性,也保证了效率。 Deleter_type _mDeleter;//删除器 public: void reset(_Ty* ps) {//值重置。 if (_mPtr != nullptr && _mRep != nullptr && _mRep->Decref() == 0) {//回收的前提是它要存在!//首先得保证这个共享型智能指针必须有值,没有值那不叫重置,那叫做赋值,另外要保证它的完整性,要有计数器,才能知道有几个指向这同一块内存 //另外如果除了它自己,没有其他人(共享指针)指向这块内存的话,直接将这个堆内存进行释放。 _mDeleter(_mPtr); if (_mRep->Decwref() == 0) {//如果连弱引用都没有了,那么计数器此时两个值都为0,那么就没有存在的必要了,此时直接将计数器的堆内存释放掉(堆内存都已经没了)。 delete _mRep; } } _mPtr= nullptr;//指向也直接置为空 _mRep=nullptr;//计数器也置为空。 //进行重置操作。 //空的不要 if (nullptr != ps) { _mPtr = ps; _mRep = new My_ReCount<_Ty>(_mPtr);//重新分配一个计数器。 } } my_shared_ptr(pointer ps = nullptr) : _mPtr(ps), _mRep(nullptr) {//地址的赋值已经在这里写了 if (nullptr != _mPtr) { _mRep = new My_ReCount<_Ty>(_mPtr);//这里是分配一个计数器 } } ~my_shared_ptr() { reset(nullptr); } //拷贝构造函数的重载 my_shared_ptr(const my_shared_ptr& other) :_mPtr(other._mPtr), _mRep(other._mRep) { if (_mPtr != nullptr) { _mRep->Incref();//因为这是拷贝构造函数,它之前是不存在的肯定没有指向其他内存空间,所以不用进行之前的减一操作,只用进行加一操作。 } } my_shared_ptr(my_shared_ptr&& other): _mPtr(other._mPtr), _mRep(other._mRep) { //转移资源 other._mPtr = nullptr; other._mRep = nullptr; } void swap(my_shared_ptr& other) { std::swap(_mPtr, other._mPtr); std::swap(_mRep, other._mRep); } //面试重点: //移动赋值以及赋值函数重载 //赋值重载函数为什么难?是因为引用计数块,或者说者两个对象对应的内存分别被其他多个对象分别指向,也有可能都为空值,情况非常非常多。 // 因为共享指针是不能这样子的,Int * p= new Int(10); my_shared_ptr<Int> pa(p); my_shared_ptr<Int> pb(p);此时pa和pb指向同一块内存,但是这是两个计数器,这两个计数器在堆上的地址是各不相同的。 // 这也是十分危险的操作!我们默认是不可以进行这样操作的。 //正常情况下就不可能出现地址为空,计数器不为空,以及地址或者地址不为空,计数器为空的情况,只有两者都为空和两个不为空这两种状态。 my_shared_ptr& operator=(const my_shared_ptr& other) { if (this != &other) { if (this->_mPtr == nullptr&& other._mPtr != nullptr) { this->_mPtr = other._mPtr; this->_mRep = other._mRep; this->_mRep->Incref(); } else if (other._mPtr != nullptr) { //最常见的情况 my_shared_ptr<Int>(other).swap(*this); //copt加上swap的巧妙之处在于完美的利用了临时对象生命周期的特点,先创建一个中间对象,然后指向other,other引用计数加一,此时这个临时对象自动调用了swap函数,此时是临时对象和当前类进行进行 //资源的互换,此后,临时对象存储的是当前类之前的资源,当前类指向的是other类对象,然后程序结束后,这个临时对象回收,自动调用析构函数,同时调用reset函数,进行计数器减一操作 //通过这一行代码,完美实现了交换资源加上对应计数器的加一减一操作,十分的巧妙。 return *this; } } return *this; } my_shared_ptr& operator=(my_shared_ptr&& other) { //最常见的情况 //同样的道理,只不过这里通过右值拷贝构造函数的特性先将other的资源转移到中间变量tmp暂时接管,此时这个other变为计数器和指向地址都为空的中间状态,此时这个中间变量调用 //swap函数与当前类进行资源互换,此时这个other指向的资源一直处于转移状态所以,并没有导致计数器发生变化,但是此时当前对象的旧资源由tmp接管,它析构的时候会自动将这个旧资源 //的计数器进行减一操作。 if (this != &other) { if (this->_mPtr == nullptr&& other._mPtr != nullptr) { swap(this->_mPtr, other._mPtr); swap(this->_mRep, other._mRep); } else if (other._mPtr != nullptr) { my_shared_ptr<Int>(std::move(other)).swap(*this); } } return *this; } };

核心知识点解析 含面试重点

5.1 核心成员变量设计 面试高频

  • _mPtr:指向用户真实堆资源的指针;_mRep:堆上的引用计数控制块指针。

  • 面试题解答:为什么计数器用堆对象(指针),不用栈值?如果使用栈值计数器,每个智能指针都有独立计数器,多指针共享同一块内存时,需要逐个修改计数器,系统开销极大。 使用堆区控制块,多个shared_ptr共享同一个计数器,全局统一修改计数,保证时效性与运行效率。

  • _mDeleter:通用删除器仿函数,负责最终内存释放。

5.2 reset重置函数核心逻辑

  • 重置前置条件:智能指针必须持有有效资源、有效计数器,才能执行资源回收。

  • 资源释放逻辑:强引用计数归0 → 调用删除器释放用户堆内存 → 递减弱引用计数;若弱引用也归0,彻底销毁计数器控制块。

  • 重置后置操作:将自身指针、计数器全部置空,再绑定新的内存与计数器,状态干净无残留。

5.3 生命周期函数逻辑

  • 构造函数:创建智能指针,绑定堆资源,自动创建对应的引用计数控制块。

  • 析构函数:直接调用reset(nullptr),依托reset逻辑自动处理计数递减、资源释放、控制块销毁。

  • 拷贝构造:共享资源与计数器,仅执行强引用计数+1,无需释放任何资源。

  • 移动构造:直接转移资源与计数器所有权,原对象置空,计数不变,零开销转移。

5.4 赋值重载核心难点 面试重中之重

  • 赋值重载难点:两个智能指针可能各自被多个对象引用、可能为空,场景极其复杂,极易出现计数错乱、内存泄漏、重复释放。

  • 禁止危险写法:裸指针多次初始化shared_ptr,会创建多个独立计数器,导致重复释放崩溃。

  • 拷贝赋值巧妙逻辑:利用临时对象+swap机制 1. 临时对象拷贝other资源,计数+1; 2. 临时对象与当前对象交换资源; 3. 临时对象生命周期结束,析构自动释放当前对象旧资源、计数-1; 一行代码完美完成新旧资源更替、计数增减,无冗余逻辑。

  • 移动赋值巧妙逻辑:利用move转移资源所有权,通过临时对象接管旧资源,析构自动回收旧资源,保证计数准确性。

六、多场景测试用例 全部保留原生代码

6.1 主测试用例 默认执行

int main() { my_shared_ptr<Int> pa,pb; pa = pb; my_shared_ptr< my_shared_ptr<Int> pd(new Int(100)); my_> pe(pd); pd = pc; return 0; }shared_ptr<IntInt> pc(new Int(10));

测试场景解析

  • 空指针赋值测试:两个空shared_ptr互相赋值,无资源操作,安全无报错。

  • 多对象共享测试:pd、pe共享同一块内存,引用计数正常递增。

  • 资源替换赋值:pd从指向100的对象,赋值改为指向10的对象,旧资源计数递减、新资源计数递增。

6.2 拷贝移动复合测试用例 条件编译

#if 0 int main() { my_shared_ptr<Int> pa(new Int(10)); my_sha<Int> pb(pa); my_shared_ptr<Int> pc(std::move(pa)); } #endif; return 0red_ptr

测试场景解析

  • pa初始化堆对象,强引用计数=1。

  • pb拷贝构造pa,共享资源,强引用计数=2。

  • pc移动构造pa,pa资源被转移置空,计数不变,仅所有权转移。

6.3 局部作用域计数销毁测试用例 条件编译

#if 0 int main() { my_shared_ptr<Int> pa(new Int(10)); my_shared_ptr<Int> pb(pa); { my_shared_ptr<< "-----------------{}-------------" << endl; } #endif return 0; }Int> pc(pb); cout <

测试场景解析

  • 利用局部代码块作用域特性,pc为局部对象,出作用域自动析构。

  • pc析构触发计数递减,验证局部对象销毁不影响全局共享资源,仅引用计数变更。

  • 直观体现shared_ptr基于计数的资源生命周期管理核心。

七、全文核心总结

  • shared_ptr核心本质:共享所有权智能指针,通过堆上原子引用计数控制块,实现多指针共享同一块堆资源,线程安全。

  • 计数设计精髓:强弱双原子计数,强引用管控资源生命周期,弱引用管控控制块生命周期,解决循环引用问题。

  • 资源释放规则:强引用归0释放用户堆资源,强弱引用全部归0释放计数控制块,分层释放杜绝内存泄漏。

  • 赋值重载最优解:临时对象+swap机制,极简代码完成资源更替、计数增减,规避所有边界场景报错。

  • 与unique_ptr核心区别:unique_ptr独占资源、禁止拷贝;shared_ptr共享资源、支持拷贝,依靠引用计数管理生命周期。