提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 一、核心矛盾点
- 二、错误实现演示与问题分析
- 错误代码片段
- 测试场景与崩溃后果
- 核心问题总结
- 三、正确方案的本质:用指针共享堆上的计数
- 简化优化:不用结构体,只用 `int*`
- 四、补充解答:结构体是必须的吗?
- 五、总结
我有一个疑问RefCount必须以一个类或者结构体放置在外面吗,这里面本质不就是一个计数吗,能不能把这个计算int类型变量直接放置在sharedptr类中作为成员变量呢,请说明行或者不行及其原因
结论先行:绝对不行,不能将引用计数直接作为SharedPtr的普通int成员变量,这违背了shared_ptr共享计数的核心设计原理。下面结合底层逻辑、错误案例、后果分析,为你详细解释原因,同时给出简化的正确实现方案。
一、核心矛盾点
shared_ptr的设计目标:多个智能指针对象共享同一个资源,且共用同一个引用计数,计数的增减对所有共享该资源的指针都生效。
- 类的普通成员变量(值类型):属于对象独占,每个
SharedPtr实例都会独立拥有一份成员变量副本,彼此隔离、无法同步; - 引用计数的需求:必须是全局唯一、所有共享实例都能访问修改的共享变量。
这是直接用int成员变量无法解决的本质矛盾。
二、错误实现演示与问题分析
我们模拟你设想的写法,把计数直接作为成员变量,看看会发生什么致命问题:
错误代码片段
template<typenameT>classBadSharedPtr{private:T*ptr_;intref_count_;// 直接将计数作为普通成员变量(错误写法)public:// 构造函数explicitBadSharedPtr(T*p=nullptr):ptr_(p),ref_count_(0){if(ptr_)ref_count_=1;}// 拷贝构造:拷贝指针和计数值BadSharedPtr(constBadSharedPtr&other){ptr_=other.ptr_;ref_count_=other.ref_count_;ref_count_++;// 尝试给当前对象的计数+1}// 析构函数~BadSharedPtr(){ref_count_--;if(ref_count_==0){deleteptr_;std::cout<<"资源释放\n";}}T&operator*(){return*ptr_;}intuse_count()const{returnref_count_;}};测试场景与崩溃后果
voidtest(){BadSharedPtr<int>sp1(newint(10));// sp1.ptr_ → 堆内存,sp1.ref_count_ = 1BadSharedPtr<int>sp2=sp1;// 拷贝构造:sp2.ptr_ 和 sp1 指向同一块内存// sp2.ref_count_ = 2,sp1.ref_count_ 仍然是 1(两个独立变量!)std::cout<<sp1.use_count();// 输出 1std::cout<<sp2.use_count();// 输出 2}// 函数退出,析构顺序:sp2 先析构 → sp2.ref_count=1,不释放资源// sp1 后析构 → sp1.ref_count=0,释放资源 ✔️ 看似没问题?// 进阶场景:多一层拷贝BadSharedPtr<int>sp3=sp2;// 析构时:sp3计数减到1、sp2计数减到1、sp1计数减到0 → 仅释放一次// 但如果执行 reset、赋值操作,会直接崩溃:sp1.reset();// sp1 计数减为0,释放内存;但 sp2/sp3 仍持有该内存指针,变成野指针!// sp2 析构时会再次 delete 同一块内存 → **重复释放,程序崩溃**核心问题总结
- 计数隔离,无法同步:每个对象的
ref_count_都是独立副本,修改一个不会影响其他对象,计数彻底失效; - 重复释放/内存泄漏:无法准确判断资源的真实引用数量,要么重复释放内存导致崩溃,要么计数永远不为0导致内存泄漏;
- 行为不可控:赋值、
reset、移动等操作都会产生未定义行为。
三、正确方案的本质:用指针共享堆上的计数
要实现唯一且共享的计数,必须将计数对象分配在堆内存上,所有SharedPtr实例只存储一个指针,指向这块堆内存。
所有实例通过指针访问同一个计数变量,修改后全局同步,这也是我们最初用RefCount结构体/int*的核心原因。
简化优化:不用结构体,只用int*
你之前看到的RefCount结构体是为了扩展(兼容弱引用计数),如果仅实现基础共享计数,完全可以去掉结构体,直接用int*作为成员变量,这是最精简的正确写法:
#include<iostream>#include<utility>#include<string>template<typenameT>classSharedPtr{private:T*ptr_;// 托管资源指针int*ref_cnt_;// 指针:指向堆上的唯一引用计数(核心修正)// 私有释放逻辑voidrelease(){if(ref_cnt_){--(*ref_cnt_);// 计数为0,释放资源和计数对象if(*ref_cnt_==0){deleteptr_;deleteref_cnt_;}// 清空当前对象指针ptr_=nullptr;ref_cnt_=nullptr;}}public:// 空构造SharedPtr():ptr_(nullptr),ref_cnt_(nullptr){}// 有参构造explicitSharedPtr(T*p):ptr_(p){if(ptr_){ref_cnt_=newint(1);// 堆上分配计数,初始值1}else{ref_cnt_=nullptr;}}// 拷贝构造:共享指针和计数,计数+1SharedPtr(constSharedPtr&other){ptr_=other.ptr_;ref_cnt_=other.ref_cnt_;// 共享同一个计数指针if(ref_cnt_){++(*ref_cnt_);}}// 移动构造:转移所有权,不修改计数SharedPtr(SharedPtr&&other)noexcept{ptr_=other.ptr_;ref_cnt_=other.ref_cnt_;other.ptr_=nullptr;other.ref_cnt_=nullptr;}// 拷贝赋值SharedPtr&operator=(constSharedPtr&other){if(this!=&other){release();ptr_=other.ptr_;ref_cnt_=other.ref_cnt_;if(ref_cnt_)++(*ref_cnt_);}return*this;}// 析构函数~SharedPtr(){release();}// 工具函数T&operator*()const{return*ptr_;}T*operator->()const{returnptr_;}intuse_count()const{returnref_cnt_?*ref_cnt_:0;}voidreset(T*p=nullptr){release();ptr_=p;if(ptr_)ref_cnt_=newint(1);}};四、补充解答:结构体是必须的吗?
不是必须的!
- 基础场景:用
int*就足够实现共享计数; - 扩展场景:需要弱引用计数、自定义删除器、原子操作标记等附加信息时,用结构体封装多个变量更优雅(这也是标准库和我们最初代码用
RefCount的原因); - 核心约束不变:计数必须存储在堆上,通过指针共享,不能是类的值类型成员。
五、总结
- 不行的根本原因:普通
int成员变量是对象独占的,无法让多个SharedPtr共享同一个计数,会导致计数失效、内存崩溃; - 正确实现规则:引用计数必须分配在堆内存中,所有智能指针对象通过指针共享这块内存,保证计数全局同步;
- 简化写法:无需结构体,用
int*即可实现基础功能,结构体仅用于代码扩展和规范化管理。