C++引用计数设计与分析(解决垃圾回收问题)
1.引言
上一篇博文关于浅拷贝和深拷贝 https://www.cnblogs.com/zhaoyixiang/p/12116203.html
我们了解到我们在浅拷贝时对带指针的对象进行拷贝会出现内存泄漏,那C++是否可以实现像python,JAVA一样引入
垃圾回收机制,来灵活的来管理内存。
遗憾的是C++并不像python、java等编程语言一样有着垃圾回收机制(Gabage Collector),因此导致了C++中对动态
存储的管理称为程序员的噩梦,出现了内存遗失(memory leak)、悬空指针、非法指针存取等问题。
Bjarne本人认为:
“我有意这样设计C++,使它不依赖于自动垃圾回收(通常就直接说垃圾回收)。这是基于自己对垃圾回收系统的经验,
我很害怕那种严重的空间和时间开销,也害怕由于实现和移植垃圾回收系统而带来的复杂性。还有,垃圾回收将使C+
+不适合做许多底层的工作,而这却正是它的一个设计目标。但我喜欢垃圾回收的思想,它是一种机制,能够简化设计、
排除掉许多产生错误的根源。
C++中提供的构造函数和析构函数就是为了解决了自动释放资源的需求。Bjarne有一句名言,“资源需求就是初始化(Resource Inquirment Is Initialization)”。
因此,我们可以将需要分配的资源在构造函数中申请完成,而在析构函数中释放已经分配的资源。
在C++中,允许你控制对象的创建,清楚和复制,我们就可以通过开发一种称为引用计数的垃圾回收机制实现这种控制
2.设计思想
首先我们明确在对存在指针的对象构造时,析构对象时需要把指针delete(释放掉),但是此时如果我们对对象进行浅拷贝,没有新的指针new。
析构对象时候会出现内存泄漏(一个指针所指的内存被两次释放的清况),我们用通过引用计数来解决这个问题:
每构造一个对象,就创建一个新的计数器并+1.每拷贝构造一次就在被拷贝的那个对象所在的计数器上+1;
析构时候 按照构造函数析构的顺序(后造先放,类似栈),最后构造或拷贝的先释放;
每次释放先对计数器-1并判断计数器是否为0(是否存在浅拷贝的对象),大于0时,继续按照析构顺序析构下一个对象;
当计数器为0时,释放指针。
3.举例
我们按顺序构造3个对象,计数器标号记为 1,2,3,对第一个和第三个对象浅拷贝两次,
对对象拷贝完成后计数器1,2,3的值分别为 2 1 2.
先释放计数器3 计数器-1后等于1,析构掉一个对象。计数器为 2 1 1
再释放计数器1 计数器-1后等于1,析构掉一个对象。计数器为 1 1 1
再释放计数器3 计数器-1后等于0,析构掉一个对象,并释放掉指针。计数器为 1 1 空
再释放计数器2 计数器-1后等于0,析构掉一个对象,并释放掉指针。计数器为 1 空 空
再释放计数器1 计数器-1后等于0,析构掉一个对象,并释放掉指针。计数器为 空 空 空
最终所有对象析构完毕,指针也全部释放完
4.代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | //引用计数类 class CRefCount { public : CRefCount(); //构造计数器对象 CRefCount( const CRefCount& obj); //拷贝构造计数器 void * Alloc( int size); //构造对象时申请空间 int AddRef(); //计数增加 int ReleaseRef(); //计数减少 ~CRefCount(); private : void * m_pBuf; //指针缓冲区 int * m_pRefCount; //计数 }; CRefCount::CRefCount() { m_pBuf = nullptr ; m_pRefCount = nullptr ; } CRefCount::CRefCount( const CRefCount& obj) { m_pBuf = obj.m_pBuf; m_pRefCount = obj.m_pRefCount; AddRef(); } void * CRefCount::Alloc( int size) { m_pBuf = new char [size + 1]; //申请缓冲区 m_pRefCount = new int (0); AddRef(); //每次构造对象计数+1 return m_pBuf; } int CRefCount::AddRef() { if (m_pRefCount == nullptr ) return 0; return ++(*m_pRefCount); } int CRefCount::ReleaseRef() { if (m_pRefCount == nullptr ) return 0; return --(*m_pRefCount); } CRefCount::~CRefCount() { if (ReleaseRef() == 0) { if (m_pBuf != nullptr ) { delete [] m_pBuf; m_pBuf = nullptr ; delete m_pRefCount; m_pRefCount = nullptr ; } } } |
5.测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | //student测试用例 #include"CRefCount.h" #include<iostream> #pragma warning(disable:4996) using namespace std; class CStudent { private : char * m_pName; CRefCount m_RefCount; const char * GetName() const ; public : CStudent( const char * pName); }; const char * CStudent::GetName() const { return m_pName; } CStudent::CStudent( const char * pName) { m_pName = ( char *)m_RefCount.Alloc( strlen (pName) + 1); //申请一个用来存放名字的空间 strcpy (m_pName, pName); } int main() { CStudent s1( "shadow" ); CStudent s2( "iceice" ); CStudent s3( "maybe" ); CStudent s4 = s1; CStudent s5 = s3; return 0; } |
调试这个程序,我们在完成构造和拷贝后,查看内存,可以看到此时计数器1,2,3分别对应的值为2,1,2
单步跟入,看到第一个拷贝构造的对象被析构掉,计数器值-1 ,此时3个计数器值分别为为2,1,1
再继续往后走,发现第二个拷贝对象析构掉切指针所指的内存还未被释放掉,计数器1 -1,此时计数器值为 1,1,1
再向后执行,此时第三个构造的对象开始被析构掉同时计数器减到0,此时对象3的指针被释放掉。
加上辅助调试代码,最终可以看到执行结果,构造3次,拷贝2次,释放3次,完成了引用计数功能
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通