浅拷贝:
class String
{
public:
String(const char *pStr = "")
:_pStr(new char[strlen(pStr) + 1])
{
strcpy(_pStr, pStr);
}
~String()
{
delete[] _pStr;
_pStr = NULL;
}
private:
char *_pStr;
};
void FunTest()
{
String s1("hello");
String s2(s1);
String s3 = s2;
}
int main()
{
FunTest();
system("pause");
return 0;
}
浅拷贝在运行过程中总是会发生崩溃,为了找出原因进行一步步调试。
这样就导致在接下来 调用析构函数释放其他对象时,去释放一段已经被释放的内存(因为他们公用一块内存)导致程序崩溃。
总结:在浅拷贝中,由于拷贝构造函数和复制运算符重载时,只把已有对象的内容赋给新创建的对象,导致多个对象公用了同一段内存,结果当任意一个对象销毁时他就会释放那段他们公用的内存,当剩下的对象在被销毁时,就回重复的释放那段内存空间,导致程序崩溃。
深拷贝
class String
{
public:
String(const char *pStr = "")
:_pStr(new char[strlen(pStr) + 1])
{
strcpy(_pStr, pStr);
}
String(const String& s)
{
_pStr = new char[strlen(s._pStr) + 1];
strcpy(_pStr, s._pStr);
}
String& operator= (String &s)
{
if (this != &s)
{
String tmp(s);
std::swap(_pStr, s._pStr);
}
return *this;
}
~String()
{
delete[] _pStr;
_pStr = NULL;
}
private:
char *_pStr;
};
void FunTest()
{
String s1("hello");
String s2(s1);
String s3 = s2;
}
int main()
{
FunTest();
system("pause");
return 0;
}
这样在对象被销毁时,调用析构函数时,释放的一定是该对象自己的那快内存空间,不会打扰到其他对象的内存,这样就避免了互相之间的干扰,程序正常执行。
可以看到,FunTest()函数调用完成之后,s1,s2,s3三个对象被成功的销毁,程序正常!
引用计数
class String
{
public:
String(const char *pStr = "")
:_pStr(new char[strlen(pStr) + 1])
, count(new int[1])
{
strcpy(_pStr, pStr);
*count = 1;
}
String(const String& s)
{
_pStr = s._pStr;
count = s.count;
(*count)++;
}
~String()
{
if (NULL != _pStr && *count == 1)
{
delete[] _pStr;
delete[] count;
_pStr = NULL;
count = NULL;
}
else if (*count > 1)
{
(*count)--;
}
}
private:
char *_pStr;
int *count;
};
void FunTest()
{
String s1("hello bit");
String s2(s1);
}
int main()
{
FunTest();
system("pause");
return 0;
}
对于浅拷贝所出现的问题,不仅仅可以使用深拷贝来解决,我们可以通过标记储存字符串那段空间的使用次数,用来真正确定,这块空间是不是真的要释放!这就是引用计数法
引用计数,它是多个对象一同进行维护的。比如创建s1之后,s1的引用计数就为1,通过s1创建s2,s1和s2共用了s1的字符串。则s1和s2的引用计数要一致为2。为了实现,所以在成员变量定义一个int* 类型的指针,这个指针指向存储引用计数的空间,多个对象指向同一块的引用计数空间时,说明他们使用的同一字符串!
创建对象s1,使用s1拷贝构造s2,可以看到他们使用的是同一字符串,同一引用计数
在函数返回之前,s2先销毁,发现s1也是用的是这块空间,所以就让引用计数减去1,不进行销毁。
不过对于引用计数存在一个很重要的缺点,就是当多个对象使用同一个字符串时,任何一个对象修改字符串里面的值都会造成其他对象所指向的字符串改变!
对于上图,s2的内容被修改之后,s1的内容也随之修改,这就是引用计数的最大不足!
写时拷贝(copy-on-write)
实现一个简单的写时拷贝:
class String
{
public:
String(char *pStr = "")
:_pStr(new char[strlen(pStr) + 4 + 1])
{
_pStr += 4;
strcpy(_pStr, pStr);
*(int *)(_pStr - 4) = 1;
}
String(const String& s)
{
_pStr = s._pStr;
int len = Size();
(*(int *)(_pStr - 4))++;
}
~String()
{
if (NULL != _pStr)
{
int len = Size();
if (len == 1)
{
delete[](_pStr + 4);
_pStr = NULL;
}
else
{
(*(int *)(_pStr - 4))--;
}
}
}
void WriteCopy()//写时拷贝
{
int len = Size();
char *tmp = new char[len + 1 + 4];
tmp += 4;
strcpy(tmp, _pStr);
if (len > 1)
{
(*(int *)(_pStr - 4))--;
std::swap(_pStr, tmp);
(*(int *)(_pStr - 4)) = 1;
}
}
size_t Size()const
{
char* tmp = _pStr;
size_t count = 0;
while (*tmp++)
{
count++;
}
return count;
}
String operator =(const String & s)
{
_pStr = s._pStr;
int len = this->Size();
(*(int *)(_pStr - 4))++;
}
char& operator [](size_t idx)
{
static char sNULL = '\0';
if (idx < Size() && idx > 0)
{
WriteCopy();
return _pStr[idx];
}
return sNULL;
}
const char& operator [](size_t idx)const
{
static char sNULL = '\0';
if (idx < Size() && idx > 0)
{
return _pStr[idx];
}
return sNULL;
}
public:
char *_pStr;
};
void FunTest()
{
String s1("hello bit");
cout << *(((int *)s1._pStr) - 1) << endl;
String s2(s1);
String s3 = s2;
cout << *((int *)s1._pStr - 1) << endl;
s3[2] = 'W';
cout << *((int *)s1._pStr - 1) << endl;
}
int main()
{
FunTest();
system("pause");
return 0;
}
先创建对象s1
通过s1创建s2
创建s3通过s2赋值给s3
改变s3的内容,这个时候就会发生写时拷贝,s3会拥有新的内存空间
s1引用计数的变化