这篇博客将一步一步写出写实拷贝的的思路,以模拟实现string类为例子
1、深浅拷贝
#include <iostream>
#include <stdlib.h>
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str)+1])
{
strcpy(_str, str);
}
//浅拷贝
String(const String&s)
{
_str = s._str;
}
////深拷贝
//String(const String&s)
// :_str(new char[strlen(s._str)+1])
//{
// strcpy(_str, s._str);
//}
~String()
{
if (_str)
{
delete[] _str;
}
}
private:
char*_str;
};
int main()
{
String s1("123455");
String s2(s1);
return 0;
}
上面的程序使用浅拷贝将会崩溃。因为浅拷贝是直接指向了同一块空间,在调用析构函数的时候会对同一块空间delete[]了两次。这样是不合法的。使用深拷贝将会没有任何问题。
使用了深拷贝,其实就是另外开辟一块空间,再将原来空间的值拷贝下来。
2、引用计数的浅拷贝
如果我们不想用深拷贝,因为深拷贝会开辟空间,这样浪费空间,同时还影响效率。那么我们就可以使用引用计数的浅拷贝。引用计数的浅拷贝指的是当一个类还有对象存在的时候,就不进行析构。这样就避免对同一块空间的多次delete[]。
(1)引入static _refcount当做计数器
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str)+1])
{
strcpy(_str, str);
}
//浅拷贝
String(const String&s)
{
_str = s._str;
_refcount++;//进行拷贝构造,增加引用计数的个数
}
~String()
{
if (--_refcount == 0)
{
delete[] _str;
}
}
private:
char*_str;
static int _refcount;//定义为静态的
};
int String::_refcount = 1;//静态全局变量需要在类外面初始化
定义为静态的_refcount 是为了让对象共用一个。而不是每一个对象都有一个引用计数。
这样似乎是对的,但是一个类可以同时开辟出多个对象,这多个对象都有自己的空间,但是使用了静态的_refcount的话,他们都共有一个引用计数,这样依然会对同一块空间有多次的delete[]。
(2)使用指针类型的_refcount
为了克服以上的问题,我们打算开辟一个整型大小空间,用int* ——refcount
维护这块空间。这样就可以保证每一块空间搭配一个引用计数,同时同一个对象拷贝构造出来的使用的是同一个引用计数。
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str) + 1])
, _refcount(new int(1))
{
strcpy(_str, str);
}
String(const String& s)
:_str(s._str)
{
_refcount = s._refcount;
(*_refcount)++;
}
//s1 = s2
String& operator=(const String& s)
{
if (_str != s._str)
{
if (--(*_refcount) == 0)
{
delete[] _str;
delete _refcount;
_str = NULL;
_refcount = NULL;
}
_str = s._str;
_refcount = s._refcount;
++(*_refcount);
}
return *this;
}
~String()
{
if (--(*_refcount) == 0)
{
delete[] _str;
delete _refcount;
_str = NULL;
_refcount = NULL;
}
}
private:
char* _str;
int* _refcount;
};
int main()
{
String s1("123455");
String s2(s1);
String s3(s2);
//创建新的空间
String s4("0009");
String s5(s4);
return 0;
}
s1\s2\s3指向同一块空间,共用一个引用计数。s4\s5指向另外一块空间,共用另一个引用计数
(3)将引用计数放到开辟空间的前面
使用指针的引用计数是一种好的方式,但是这样会增加内存空间的碎片化。为了解决内存碎片化,我们可以在开辟的_str 空间的前面多开一个int大小的空间,用作存放引用计数。模仿new[]的做法。
修改代码如下:
class String
{
public:
String(const char* str = "")
//strlrn(str) + 1是给'\0'留下的空间,+4是给引用计数
:_str(new char[strlen(str) + 1 + 4])
{
*((int*)_str) = 1;
//让_str指向引用计数之后的空间,即字符串的开始
_str += 4;
strcpy(_str, str);
}
String(const String& s)
//让_str指向引用计数
:_str(s._str-4)
{
//引用计数加一
(*((int*)_str))++;
_str += 4;
}
//s1 = s2
String& operator=(const String& s)
{
if (_str != s._str)
{
//引用计数减一判0
if (--(*((int*)_str-1)) == 0)
{
delete[] _str;
_str = NULL;
}
_str = s._str;
//引用计数加一
(*((int*)s._str - 1))++;
}
return *this;
}
~String()
{
if (--(*((int*)_str - 1)) == 0)
{
delete[] _str;
_str = NULL;
}
}
private:
char* _str;
};
int main()
{
String s1("123455");
String s2(s1);
String s3(s2);
String s4("0009");
String s5(s4);
return 0;
}
至此,我们的引用计数的浅拷贝已经完成的很好了。
3、写时拷贝
有一个问题,因为s1、s2同时指向了同一块空间,如果我对s1进行了值的修改,必定会同时修改了s2的值。我们不希望这事情的发生,因此我们引入了写时拷贝。写时拷贝的意思是,当我需要向空间写入值的时候,即修改值的时候,我们就将修改的对象拷贝一份出来。这样就不会影响到其他的对象了。
void CopyOnWrite()
{
if (*((int*)_str-1) > 1)
{
(*((int*)_str - 1))--;
char* tmp = new char[strlen(_str) + 5];
tmp -= 4;
strcpy(tmp, _str);
_str = tmp;
(*((int*)_str+1))++;
}
}