以下我们都用string类来说明。
一. 浅拷贝
首先我们来说一说浅拷贝,浅拷贝是什么呢?浅拷贝就是指将拷贝时只将指针拷贝过来了,和被拷贝的内容指向的是同一块空间。这样就会出现不想要的结果。
- 当有一个指针想要对其这块空间进行修改,那么这时,并不是只有一个指针指向这里,而还有别的指针。所以这里是一块共享的内存空间,那么有其中一个对其改变,其余指针的内容也将受到影响,这样就会影响其它的工作。
- 如果是在对象之间发生这样的事情,有两个对象的成员变量都指向同一块内存空间,不仅会发生上面那样的事,这块空间在析构时,可能会被释放多次。就会导致程序出错。
就像下面这个例子一样:
是直接用被拷贝的字符串指针初始化拷贝的字符串。
String(const String& s)
:_str(s._str)
{}
二. 深拷贝
深拷贝就是和浅拷贝对应的一种拷贝方式,深拷贝指的是将要被拷贝的对象中的所有成员变量都拷贝一份,重新开辟一块内存空间,在这块内存上存着,自己指向自己所对应的内存空间,等到最后需要析构时,也是自己释放自己的空间,不会出现多次释放同一块空间的情况,也不会出现改了一块空间,其余的对象受影响的情况。这样的两个对象是相互独立的,只是值相同而已。
String(const String& s)
:_str(new char[s._size + 1])
,_size(s._size)
,_capacity(s._capacity)
{
strcpy(_str,s._str);
}
三. 引用计数
引用计数与写时拷贝是对深拷贝与浅拷贝的结合。为了不浪费空间,并且可以对所指的内容更改,就产生了这样的一种方法。
这次我们就可以先采用浅拷贝,若是暂时不需要对这些变量进行改变。而且需要它的值与其它的值相同,就可以给这些对象只开辟一块内存空间,让其共享。不允许对共享的区域改变。也不可以释放这块空间。所以,引入了引用计数,在其共享区域多开辟一个字节的空间,用于计数,若是超过一个对象使用,就不可以随意更改以及释放。若是只有一个对象使用,就可以更改释放。
这里有两种引用计数的方法。
- 将计数的内存与对象的成员变量分开开辟,但是这样就会多开辟一次空间,会有一定的成本。
代码如下:
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str)+1])
,_pCount(new size_t(1))
{
strcpy(_str,str);
}
// s2(s1)
String(const String& s)
:_str(s._str)
,_pCount(s._pCount)
{
(*_pCount)++;
}
//s2 = s1
String& operator=(const String& s)
{
if(s._str != _str)
{
if(*_pCount == 1)
{
delete[] _str;
}
else
{
(*_pCount)--;
}
_str = s._str;
_pCount = s._pCount;
(*_pCount)++;
}
return *this;
}
~String()
{
if(*_pCount == 1)
{
*(_pCount) = 0;
delete[] _str;
delete _pCount;
}
}
const char* c_str()
{
char *ret ;
ret = new char[strlen(_str)]+1;
ret = strcpy(ret,_str);
return ret;
}
char& operator[](size_t pos)
{
assert(pos>strlen(_str));
return _str[pos];
}
private:
char* _str;
size_t _pCount;
- 将其一起开辟,在开辟的空间的开始四个字节用于计数。
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str)+5])
{
*(int*)(_str)=1;
_str = _str + 4;
strcpy(_str,str);
}
// s2(s1)
String(const String& s)
{
_str = s._str;
(*(int*)(_str-4))++;
}
//s2 = s1
String& operator=(const String& s)
{
if(s._str != _str)
{
if(*(int *)(_str - 4) == 1)
delete[] (_str - 4);
_str = s._str;
*(int *)(_str - 4) += 1;
}
return *this;
}
~String()
{
if(*(int *)(_str - 4) == 0)
delete[] (_str-4);
}
const char* c_str()
{
char* ret = new char[strlen(_str)+1];
strcpy(ret,_str);
return ret;
}
char& operator[](size_t pos)
{
return _str[pos];
}
private:
char* _str; // 引用计数在头上
};
四. 写时拷贝
上面说了,不允许在共享的区域对公共部分改变以及释放。这不代表着创建了对象就不可以对其再改变了,我们可以在需要对它进行改变时将它拷贝出来,就相当与一次深拷贝。再对它改变。由于有两种的计数方式,所以也就是有两种的写时拷贝。
第一种:
void CopyOnWrite()
{
char *ret = new char[strlen(_str)+1];
strcpy(ret,_str);
_str = ret;
(*_pCount)--;
*_pCount = 1;
}
第二种:
void CopyOnWrite()
{
char *tmp = new char[strlen(_str)+5];
*((int*)tmp) = 1;
*((int *)(_str)-1) -= 1;
tmp += 4;
strcpy(tmp,_str);
_str = tmp;
}
总结:这种方法既可以满足我们需要的功能,也能一定程度上节省空间。最坏的情况也就是全部进行深拷贝,和深拷贝的代价一样。但是不可能每次都是最坏的情况。所以,引用计数与写时拷贝是一种很有效的方法。这种方法在其它地方也有应用。比如linux操作系统中父子进程之间的关系。