本篇博客自底向上拆解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共享资源、支持拷贝,依靠引用计数管理生命周期。