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

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

一、前置头文件配置

cpp 复制代码
#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实体测试类 资源载体

cpp 复制代码
//-------------------------------------------------------
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,标记资源转移完成,避免资源混乱。

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

cpp 复制代码
//-------------------------------------------------------
//共享指针:
// 一个堆区资源被多个指针所指向就叫做共享指针。
//对于移动语义,只有对堆区资源有意义。
//基于共享所有权,使用引用计数控制块管理资源对象的生命期。
//在容器中保存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 线程安全核心

cpp 复制代码
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 完整实现

cpp 复制代码
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 主测试用例 默认执行

cpp 复制代码
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 拷贝移动复合测试用例 条件编译

cpp 复制代码
#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 局部作用域计数销毁测试用例 条件编译

cpp 复制代码
#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共享资源、支持拷贝,依靠引用计数管理生命周期。

相关推荐
春生野草15 小时前
个人笔记——C语言字符串、树
c语言·开发语言·笔记
众人皆醒我独醉15 小时前
TGI:HuggingFace 的官方推理服务——不止 PagedAttention,更懂模型生态
面试·llm·ai编程
风中芦苇啊15 小时前
Java EasyExcel 导入通用工具类:自定义注解映射字段 + 反射机制
java·开发语言
PieroPc15 小时前
Python 写的 Windows凭据添加工具 第二版 解决Win11 23H2, 25H2,26H1,共享文件和打印 一部份问题,不是全部!
开发语言·windows·python
众人皆醒我独醉15 小时前
vLLM:PagedAttention 如何让 LLM 推理吞吐提升 24 倍
面试·llm·ai编程
家有娇妻张兔兔15 小时前
Java 对接 PLC 主流型号最合适的方案:Apache PLC4X 实战指南
java·开发语言·plc·modbus·数据缓存·西门子s7·apache plc4x
The_Ticker16 小时前
大宗商品行情API接入教程
开发语言·python·websocket·程序人生·区块链
Infedium16 小时前
Qt嵌入式开发核心亮点:线程安全硬件交互技术
开发语言·qt
阿里嘎多学长16 小时前
2026-07-30 GitHub 热点项目精选
开发语言·程序员·github·代码托管
神奇小汤圆16 小时前
面试官突然问:“数仓哪一层最适合做RAG?”他一下被问住了…
面试