上一篇博客中我们介绍了简单赋值浅拷贝存在的问题以及它的解决方法,下面我们继续基于String类的实现来讨论引用计数和写时拷贝
关于浅拷贝和深拷贝大家可以参考我的这篇博客:
https://blog.csdn.net/Aurora_pole/article/details/80234809
以及C++动态内存管理
https://blog.csdn.net/Aurora_pole/article/details/80229603
引用计数
- 我们在学习动态内存管理的时候,说过用new s[N]的方式来开辟空间的话,实际上是开辟了N*sizeof(s)+4个字节的空间,我们通过一个简单的实例来观察一下:
String* p = new String[10];
delete[] p;
- 我们的自定义类型的大小为sizeof(char*)+sizeof(int)*2=12个字节,我们开了这样的十个空间,大小本应该是120个字节,但实际开辟的空间大小为124个字节,多出来的这4个字节是用来存放对象的个数的,以便于析构函数统计析构次数
- 既然如此,我们在构造String对象时是否也可以采用这种计数机制,每次用new开辟空间构造一个对象我们的计数就+1,析构的时候,每析构一次,计数-1,如果计数为0,则不析构,这样不也可以解决我们简单赋值浅拷贝带来的问题吗?如图示:
- String的所有赋值、拷贝构造操作,计数器都会 +1 ; string 对象析构时,如果计数器为 0 则释放内存空间,否则计数器 -1 。实现代码如下:
String(const char* s="")
:_str(new char[strlen(s)+1])
,_count(new int(1))
{
strcpy(_str, s);
}
String(const String& s)
:_str(s._str)
, _count(s._count)
{
if (this!=&s)
(*_count)++;
}
String& operator=(const String& s)
{
if (this != &s && (0 == --(*_count)))
{
delete[] _str;
delete[] _count;
_str = s._str;
_count = s._count;
(*_count)++;
}
return *this;
}
~String()
{
//若引用计数减到0,则释放空间
if (_str && (0 == --(*_count)))
{
delete[] _str;
_str = NULL;
delete[] _count;
_count = 0;
}
}
写时拷贝
- 写时拷贝:引用计数的前拷贝,也称延时拷贝
- 实现原理:写时拷贝是通过“引用计数”实现的,在分配空间的时候多分配出4个字节,用来记录有多少个指针指向这块空间,当有新的指针指向这块空间时,引用计数加1,当要释放这块空间时,引用计数减1,直到引用计数减为0时才真正释放掉这块空间。当有的指针要改变这块空间的值时,再为这个指针分配自己的空间(注意这时的引用计数变化,旧的空间引用计数减1,新分配的空间引用计数加1)
- 上面我们实现引用计数的时候,是通过独立开辟空间进行引用计数,但是这种方法有缺陷:1)每次new两份空间,创建多个对象时效率比较低;2)它多次分配小块空间,容易造成内存碎片化,导致分配不出来大块内存;所以我们可以对其进行优化,在同一份空间上进行引用计数,开辟一个空间,前 4个字节用来计数,剩下的用来存放字符串
- 代码实现:
String(const char* s = "")
:_str(new char[strlen(s)+1+4])
{
*(int*)_str = 1;
_str += 4;
strcpy(_str, s);
}
int& Count()
{
return *((int*)_str - 1);
}
String(const String& s)
:_str(s._str)
{
Count()++;
}
String& operator=(const String& s)
{
if (this != &s && --Count()==0)
{
_str -= 4;
delete[] _str;
_str = NULL;
_str = s._str;
Count()++;
}
return *this;
}
- 注意1:如果引用计数大于1,在写之前必须拷贝这块内存单元,这样就不会影响其他对象使用这块内存了。(因为可能不止一个对象会使用这块内存,修改了自己等于修改了别人,所以再向这块内存写之前必须要确保没有其他对象使用它)
- 注意2:由于计数器存放在了_str首地址-4的地址上,所以在析构时一定要注意全部释放,避免内存泄漏