String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)

时间:2020-12-10 19:50:11

String类:标准库类型string类表示可变长的字符序列,定义在std中,专门用来管理字符串,下面一起看下它的重要考点。



一,浅拷贝

      所谓浅拷贝,是指原对象与拷贝对象公用一份实体,仅仅是对象名字不同而已(类似引用,即对原对象起别名),其中任何一个对象改变都会导致其他的对象也跟着它变。如下面这段代码:

//浅拷贝
class String
{
public:
String(const char* pStr = "")//构造函数
:_pStr(new char[strlen(pStr)+1])
{
if(0 == *pStr)//字符串为空
{
*_pStr = '\0';
}
else//字符串不为空
{
strcpy(_pStr,pStr);
}
}
String(const String& s)//拷贝构造函数
{
_pStr = s._pStr;
}
String& operator=(String& s)//赋值运算符重载
{
if(_pStr != s._pStr)//判断是不是自己给自己赋值
{
_pStr = s._pStr;
}
return *this;
}
~String()//析构函数
{
if(NULL == _pStr)
{
return;
}
else
{
delete []_pStr;
_pStr = NULL;
}
}
private:
char* _pStr;
};

void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
String s4;//s4对象已经存在了
s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
Funtest();
system("pause");
return 0;
}

一运行你就会发现,从程序开始运行到s4创建并赋值,程序都没有问题。

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)


再往下走,进入析构函数

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)


根据栈空间先入后出的原则,应该先析构s4对象,可是有上图可以看到,当释放s4后,前面创建的3个对象也都成了随机值,那么再往下走会发生什么呢?

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)

没错,程序在运行到370行是崩溃了,这是为什么呢?请看下图。

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)

从上我们可以看出浅拷贝存在一定的问题,那么怎样对它进行改进防止一个对象被多次释放呢,你可能会这样想。


//浅拷贝(引用计数(_count作为普通成员变量)error)
class String
{
public:
String(const char* pStr = "")//构造函数
:_pStr(new char[strlen(pStr)+1])
,_count(0)//初值赋值为0
{
if(0 == *pStr)
{
*_pStr = '\0';
}
else
{
strcpy(_pStr,pStr);
}
_count++;//每创建一个对象计数器加1
}
String(String& s)//拷贝构造
:_count(s._count)//将已存在的对象s的计数器赋给当前对象
{
_pStr = s._pStr;
s._count++;//将原对象的计数器加1
_count = s._count;//将原对象的计数器加1后赋值给当前对象
}
~String()//析构函数
{
if(NULL == _pStr)
{
return;
}
else
{
if(--_count == 0)//如果计数器为0,说明无对象指向该空间,可以释放
{
delete []_pStr;
_pStr = NULL;
}
}
}
String& operator=(String& s)//赋值运算符重载
{
if(_pStr != s._pStr)
{
_pStr = s._pStr;
s._count++;//将原对象的计数器加1
_count = s._count;//将已存在的对象s的计数器赋给当前对象
}
return *this;
}
private:
int _count;//给一个计数器控制析构函数
char* _pStr;
};


void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
String s4;//s4对象已经存在了
s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
Funtest();
system("pause");
return 0;
}

用一个计数器来控制析构函数听起来好像可以解决上述问题,可实际呢,结果却是这样的。


4个对象创建后:

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)


调用4次析构函数之后:

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)

之后函数就返回了。


归结一下上述问题的错误:

      我们知道,这四个对象本来指向同一块空间,计数器本来都应为4,可是现在的结果却是计数器只能控制与它相邻对象的计数器,对象创建完成后,计数器并不统一。

     其次,调用4次析构函数之后,本来应该四个对象同时被释放,可是结果却是没有一个对象的计数器为0,也就是这块空间并没有被释放,内存又泄露了呗。


为了保持计数器的统一,我们决定把计数器设置为类的静态成员函数,

//浅拷贝(引用计数(_count作为静态成员变量))
class String
{
public:
String(const char* pStr = "")//构造函数
:_pStr(new char[strlen(pStr)+1])
{
if(0 == *pStr)
{
*_pStr = '\0';
}
else
{
strcpy(_pStr,pStr);
}
_count++;
}
String(const String& s)//拷贝构造
{
_pStr = (char*)s._pStr;
s._count = _count;
_count++;
}
~String()//析构函数
{
if(NULL == _pStr)
{
return;
}
else
{
if(--_count == 0)
{
delete []_pStr;
_pStr = NULL;
}
}
}
String& operator=(String& s)//赋值运算符重载
{
if(_pStr != s._pStr)
{
_pStr = s._pStr;
s._count = _count;
_count++;
}
return *this;
}
private:
static int _count;
char* _pStr;
};

int String::_count = 0;

void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
String s4;//s4对象已经存在了
s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
Funtest();
system("pause");
return 0;
}

当函数运行到创建好s4还没有对其赋值时,

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)


再往下执行:

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)


由上图可知,计数器虽然统一了,然而我们的代码依旧存在bug。


错误分析:

1.我们一共只创建了4个对象,可是计数器却为5,那是因为静态成员变量为所有对象共享,任何对象都可以对它进行修改,每创建一个对象我们都对计数器加1,却忽略了创建的新对象是否与已存在的对象占同一块空间

2.调用4次析构函数后,计数器值为1,导致空间又没有被释放。


吸取上面的错误教训,我们又想到了指针。

//浅拷贝(引用计数(指针实现计数))
class String
{
public:
String(const char* pStr = "")//构造函数
:count(new int(0))
,_pStr(new char[strlen(pStr)+1])
{
if(NULL == pStr)
{
*_pStr = '\0';
}
else
{
strcpy(_pStr,pStr);
}
*count = 1;
}
String(const String& s)//拷贝构造
:count(s.count)
{
_pStr = (char*)s._pStr;
count = s.count;
(*count)++;
}
~String()//析构函数
{
if(NULL == _pStr)
{
return;
}
else
{
if(--(*count) == 0)
{
delete[]count;//勿忘了释放计数器指针
delete[]_pStr;
_pStr = NULL;
count = NULL;
}
}
}
String& operator=(String& s)//赋值运算符重载
{
if(_pStr != s._pStr)
{
_pStr = s._pStr;
count = s.count;
(*count)++;
}
return *this;
}
private:
int* count;
char* _pStr;
};


void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
String s4;//s4对象已经存在了
s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
Funtest();
system("pause");
return 0;
}
运行过程:

创建4个对象后:

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)

给s4赋值

后:

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)


调用4次析构函数之后:

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)


空间被准确释放,代码写到这里,我们会满意吗?当然不,永远记住:只有更好没有最好。指针计数的缺陷:每个对象都得为它多创建一个指针,浪费空间。还有释放麻烦,很有可能我们只记得释放_pStr,却忘了释放计数器指针造成内存泄漏,得不偿失。


接下来,对浅拷贝再次进行优化。

二,写时拷贝(Copy-On-Write):

       即写时才拷贝,以上诸多问题让我们得知浅拷贝容易出现指针悬挂的问题,但是我们可以应用引用计数但是方式来解决浅拷贝中多次析构的问题,同时写时拷贝就应运而生了。


//写时拷贝(仿照new[]实现)
class String
{
public:
String(const char* pStr = "")//构造函数
:_pStr(new char[strlen(pStr) + 4 + 1])//每次多创建4个空间来存放当前地址有几个对象
{
if(NULL == pStr)
{
(*(int*)_pStr) = 1;//将前4个字节用来计数
_pStr += 4;//指针向后偏移4个字节
*_pStr = '\0';
}
else
{
(*(int*)_pStr) = 1;//将前4个字节用来计数
_pStr += 4;//指针向后偏移4个字节
strcpy(_pStr,pStr);//将pStr的内容拷贝到当前对象的_pStr中
}
}
String(const String& s)//拷贝构造
:_pStr(s._pStr)
{
++(*(int*)(_pStr-4));//向前偏移4个字节将计数加1
}
~String()//析构函数
{
if(NULL == _pStr)
{
return;
}
else
{
if(--(*(int*)(_pStr - 4)) == 0)//向前偏移4个字节判断计数是否为0,是0则释放
{
delete (_pStr-4);
_pStr = NULL;
}
}
}
String& operator=(const String& s)//赋值运算符重载
{
if(_pStr != s._pStr)
{
if(--(*(int*)(_pStr - 4)) == 0)//释放旧空间
{
delete (_pStr-4);
_pStr = NULL;
}
_pStr = s._pStr;//指向新空间
++(*(int*)(_pStr - 4));//计数加1
}
return *this;
}
char& operator[](size_t index)//下标访问操作符重载
{
assert(index>=0 && index<strlen(_pStr));
if(*((int*)(_pStr-4)) > 1)//说明有多个对象指向同一块空间
{
char* temp = new char[strlen(_pStr) + 4 + 1];//新开辟一块空间
temp += 4;//先将开辟的空间向后偏移4个字节
strcpy(temp,_pStr);//将_pStr的内容拷贝到temp中
--(*(int*)(_pStr-4));//将原来空间的计数器减1

_pStr = temp;//将当前对象指向临时空间
*((int*)(_pStr-4)) = 1;//将新空间的计数器变为1
}
return _pStr[index];
}
private:
char* _pStr;
};

void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
s3[2] = 'g';
String s4;//s4对象已经存在了
s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}

int main()
{
Funtest();
system("pause");
return 0;
}

运行过程:

创建好s1,s2,s3后:

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)


改变s3的内容:

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)


s3的值被改变后:

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)


对s4进行赋值后:

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)


如果还不理解,请看下图:

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)


三,深拷贝

        所谓深拷贝,就是为新对象开辟一块新的空间,并将原对象的内容拷贝给新开的空间,释放时就不会牵扯到多次析构的问题


//深拷贝(普通版)
class String
{
public:
String(const char* pStr = "")//构造函数
:_pStr(new char[strlen(pStr)+1])
{
if(0 == *pStr)
{
*_pStr = '\0';
}
else
{
strcpy(_pStr,pStr);
}
}
String(const String& s)//拷贝构造
:_pStr(new char[strlen(s._pStr)+1])
{
strcpy(_pStr,s._pStr);
}
String& operator=(const String& s)//赋值运算符重载
{
if(_pStr != s._pStr)//判断是否自己给自己赋值
{
char* temp = new char[strlen(s._pStr)+1];//先开辟一段新空间
strcpy(temp,s._pStr);//将原对象的值赋给新空间
delete []_pStr;//释放当前对象
_pStr = temp;//将当前对象指向新开辟的空间
}
return *this;
}
~String()//析构函数
{
if(NULL == _pStr)
{
return;
}
else
{
delete[]_pStr;
_pStr = NULL;
}
}
private:
char* _pStr;
};

void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
String s4;//s4对象已经存在了
s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
Funtest();
system("pause");
return 0;
}

运行过程:

String类详解(浅拷贝,深拷贝,引用计数,写时拷贝)


仔细观看上图,你会发现虽然4个对象指向的内容一样,不过地址却均不相同,说明它们各自占各自的空间,出来值相同外,没什么联系,析构起来当然也不会有问题了。


下面看下简洁版的深拷贝:

//深拷贝(简化版1)
class String
{
public:
String(const char* pStr = "")//构造函数
:_pStr(new char[strlen(pStr)+1])
{
if(0 == *pStr)
{
*_pStr = '\0';
}
else
{
strcpy(_pStr,pStr);
}
}
String(const String& s)//拷贝构造
:_pStr(new char[strlen(s._pStr)+1])
{
strcpy(_pStr,s._pStr);
}
~String()//析构函数
{
if(NULL == _pStr)
{
return;
}
else
{
delete[]_pStr;
_pStr = NULL;
}
}
String& operator=(const String& s)//赋值运算符重载
{
if(_pStr != s._pStr)
{
delete[]_pStr;
_pStr = new char[strlen(s._pStr)+1];
strcpy(_pStr,s._pStr);
}
return *this;
}
private:
char* _pStr;
};

void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
String s4;//s4对象已经存在了
s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
Funtest();
system("pause");
return 0;
}


//深拷贝(简洁版2)
class String
{
public:
String(const char* pStr = "")//构造函数
:_pStr(new char[strlen(pStr)+1])
{
if(0 == *pStr)
{
*_pStr = '\0';
}
else
{
strcpy(_pStr,pStr);
}
}
String(String& s)//拷贝构造
:_pStr(NULL)//防止交换后temp指向随机空间,本函数调用结束导致内存泄漏以致崩溃
{
String temp(s._pStr);//如果不给出临时变量,交换后s的值将为NULL
std::swap(_pStr,temp._pStr);
}
//1
String& operator=(const String &s)//赋值运算符重载
{
if(_pStr != s._pStr)
{
String temp(s._pStr);//如果不给出临时变量,交换后s的值将为NULL
std::swap(_pStr,temp._pStr);
}
return *this;
}
/* 2
String& operator=(const String& s)
{
if (this != &s)
{
String temp(s);
std::swap(_pStr, temp._pStr);
}
return *this;
}*/

/* 3
String& operator=(String temp)
{
std::swap(_pStr, temp._pStr);
return *this;
}*/
~String()//析构函数
{
if(NULL == _pStr)
{
return;
}
else
{
delete[]_pStr;
_pStr = NULL;
}
}
private:
char* _pStr;
};

void Funtest()
{
String s1("abcd");
String s2(s1);
String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
String s4;//s4对象已经存在了
s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
Funtest();
system("pause");
return 0;
}


Fighting!!!