以String类浅谈深浅拷贝——C++
提到字符,我们会想到字符串以及字符数组, 字符数组就是用数组储存的字符,而对于字符串,在C语言中,字符串有个特点
就是以'\0'结尾,也叫做C风格的字符串,在C语言中也有很多对字符串进行操作的函数,比如:strlen,strcpy,strcmp......等等。而在
C++中,对于字符串是用String类来进行管理。
那首先,我们来模拟实现String类:
class String{
public:
char* _pStr;
public:
String(const char* pStr = "")//把缺省值设置为空,那么在就可以把不给参数的构造合并起来
{
if(NULL == pStr)//在字符串为空的时候,给结尾赋'\0',符合C风格字符串
{
_pStr = new char[1];//为下一句赋值开辟空间
*_pStr = '\0';
}
else{
_pStr = new char[strlen(pStr)+1];//在传值进行创建对象时,先开辟空间在赋值
strcpy(_pStr,pStr);//字符串赋值,调用专用的函数
}
}
~String()
{
if(_pStr)//如果对象的_pStr成员掌管空间的时候,进入循环,进行销毁
{
delete[] _pStr;
_pStr = NULL;
}
}
//拷贝构造
String(const String& s):_pStr(s._pStr)//在初始化列表中对_pStr进行赋值
{}
//赋值运算符重载
String& operator=(const String& s)
{
if(this != &s)//判断是不是自己给自己赋值,如果不是,就进入循环
{
_pStr = s._pStr;
}
return *this;
}
};
这段代码编译是没有问题的,但是运行起来会崩溃,引起这个崩溃的原因就是在拷贝对象和赋值运算符进行重载的时候发
生了前拷贝的问题,因为我们在用之前的对象创建新的对象的时候,并没有为新的对象开辟空间,只是用两个对象指向了同一
块
空间,那么当一个一个销毁的时候,销毁了最后一个对象的空间之后,其余对象的空间也就被销毁了,此时,再去销毁其他
对象的空间的时候,就会出现程序崩溃。
图解如下:
这就是所谓的浅拷贝,此时的类在应用的时候就会出现问题。
所以要改掉这个问题,就要针对两个对象同用一块空间来进行改进:
改进之后代码:
(改进之后得到代码,构造和析构的函数和之前的版本没有变化,唯一有区别的地方就是在拷贝构造函数和赋值运算符重载函数
里面,给对象的成员_pStr开辟新的空间来储存值)
class String{
public:
char* _pStr;
public:
String(const char* pStr = "")//把缺省值设置为空,那么在就可以把不给参数的构造合并起来
{
if(NULL == pStr)//在字符串为空的时候,给结尾赋'\0',符合C风格字符串
{
_pStr = new char[1];//为下一句赋值开辟空间
*_pStr = '\0';
}
else{
_pStr = new char[strlen(pStr)+1];//在传值进行创建对象时,先开辟空间在赋值
strcpy(_pStr,pStr);//字符串赋值,调用专用的函数
}
}
~String()
{
if(_pStr)//如果对象的_pStr成员掌管空间的时候,进入循环,进行销毁
{
delete[] _pStr;
_pStr = NULL;
}
}
//拷贝构造
//浅拷贝里面是对对象直接赋值,所以造成两个对象共用一块空间,现在在新拷贝构造函数里面,
//我们要在初始化列表里面给_pStr开辟空间,“+1”是因为在使用strcpy函数,要在最后加上‘\0’,
//所以要多开辟一个字节的空间
String(const String& s):_pStr(new char[strlen(s._pStr)+1])
{
strcpy(_pStr,s._pStr);//开辟好空间之后,在去给对象赋值
}
//赋值运算符重载
//赋值运算符的重载和拷贝构造函数的处理方法一样,要先去给_pStr开辟空间
String& operator=(const String& s)
{
if(this != &s)//判断是不是自己给自己赋值,如果不是,就进入循环
{
char* strTemp = new char[strlen(s._pStr)+1];//创建一个临时变量,用来接收赋值对象的值
strcpy(strTemp,s._pStr);//对临时变量进行赋值
delete[] _pStr;//销毁对象成员之前掌管的那一块空间
_pStr = strTemp;//用临时的变量去给成员变量_pStr进行赋值
}
return *this;
}
};
在这段代码里面,对象_pStr掌管着自己的空间,即使用别的对象赋值,也不会发生两个对象掌管一块空间的值,此时就解
决了浅拷贝中两个对象掌管一块空间的值的问题。
上面我们用的深拷贝来解决浅拷贝的问题,但是深拷贝中,会给新的对象开辟空间,有些时候,我们在程序运行的期间,
并
不需要开辟空间,只需要这个对象开辟出来就行,此时我们引用了新的方法来解决浅拷贝所出现的问题,就是:引用计数。
在对象的创建的时候,用两个成员变量,分别指向两块空间,一块空间用来掌管我们的字符串,另外的一块空间,用来存放
的
一个数字,这个数字用来存放掌管这块空间的对象的个数。
class String{
private:
int* _pCout;
char* _pStr;
};
_pCount就是用来掌管引用计数的空间
_pStr是用来掌管字符串的空间
图解如下:
此时我们s1来拷贝构造s2, 语句:String s2(s1);
创建成功之后,结构如图:
此时我们要注意的就是,在构造的时候,要给引用计数赋值;拷贝构造的时候,要给引用计数加一,因为用拷贝构造赋值之后,一块空间就有两个对象在掌管,赋值运算符重载函数的情况,稍微比较多,我们稍后详细讲解;析构函数中,在销毁对象的时候,就要判断空间是否别的对象也在用,如果也在用就不能去销毁。
引用计数的String类的实现代码:
class String{
public:
//初始化列表中给_pCount开辟空间,因为无论是不是空串,引用计数都是1
String(const char* pStr = ""):_pCout(new int(1))
{
if (NULL == pStr)//如果是空串,那么就在结尾赋‘\0’
{
_pStr = new char[1];
*_pStr = '\0';
}
else
{
_pStr = new char[strlen(pStr)+1];
strcpy(_pStr,pStr);
}
*_pCout = 1;//创建对象完成之后,给_pCount赋值1
}
~String()
{
//如果析构的对象掌管了空间,且_pCount减1之后为零,说明之后该对象一个在用着这块空间,此时调用析构
if(_pStr && 0 == --(*_pCout))
{
delete[] _pStr;
delete _pCout;
_pStr = NULL;
_pCout = NULL;
}
}
//用赋值对象s的_pStr和_pCount赋值给正在创建的对象,
String(const String& s):_pCout(s._pCout),_pStr(s._pStr)
{
++(*_pCout);//当两个对象掌管一块对象的时候,此时引用技术加1
}
//赋值运算符重载
String& operator=(const String& s)
{
//此处检查是不是自己给自己赋值没有用:this != &s
//是因为创建的两个对象,有可能对象的位置不同,但是对象里面_pStr指向的空间有可能是一样。
if(_pStr != s._pStr)
{
//赋值之前,首先检测之前的对象有没有掌管着空间,如果有那就先销毁之前的空间,否则就会有内存泄漏
if(_pStr && 0 == --(*_pCout))
{
delete[] _pStr;
delete _pCout;
}
//销毁之前对象的空间,接下来就是给对象的空间赋值
_pStr = s._pStr;
_pCout = s._pCout;
//赋值之后,空间就多了一块掌管者,此时_pCount就要+1
(*_pCout)++;
}
return *this;
}
private:
int* _pCout;
char* _pStr;
};
此时,当两个对象掌管一块空间的时候,在依次析构的时候,就会检测空间的掌控者是不是只有一个,如果只有一个,那就销毁这块空间;
这也就是在string类里面的深浅拷贝。
限于编者水平,文章难免有缺漏之处,欢迎指正。
Tip:如许转载,请注明出处。