本篇博客通过使用string类型介绍拷贝方式。
在实现string类的过程来编写拷贝构造以及运算符重载时,我们发现可以编写的拷贝方式有浅拷贝、深拷贝以及写时拷贝。下面就会就这三种拷贝方式来进行分别讲解应分别在何种情况使用:
浅拷贝
浅拷贝就其他两种而言就相当的简单了,其编写的代码如下:
String::String(const String &s)
: _str(s._str)
, _size(s._size)
, _capacity(s._capacity)
{}
String & String::operator=(const String &s)
{
if (_str != s._str)
{
delete[] _str;
_str = s._str;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
虽然这种方法非常简单,但这种方法存在很大的漏洞,这种方法拷贝构造以及赋值运算符的重载后的对象与被拷贝的对象指向同一块空间,如果使用其中一个对象对其中的值进行修改时会导致另一个对象中的值也会发生改变,所以一般情况下不会使用浅拷贝这种拷贝方式。
但当对象中的值不能进行改变是一个const常量时,对象只可进行读不能进行修改,使用浅拷贝可减少内存的开销,不会有问题。
深拷贝
通过了解浅拷贝,我们发现浅拷贝的问题在于没有新空间的开辟所导致不能对对象中的值进行改变。但我们大部分的对象中的值都会需要进行修改,所以深拷贝就是为了解决这问题而出现的。既然指向空间相同就不能随意更改对象中内容,那么深拷贝就先给需要拷贝的对象开辟空间,再将内容拷贝过去。深拷贝分为传统写法以及现代写法这两种方法,其编写的代码如下:
class String
{
public:
String(const char* str = "");
String(const String& s);
String& operator=(const String& s);
~String();
private:
char* _str;
};
//传统写法完成String深拷贝
//String::String(const char* str)
// : _str(new char[strlen(str) + 1])
//{
// strcpy(_str, str);
//}
//
//String::String(const String& s) // 必须用引用否则会无限的递归
// : _str(new char[strlen(s._str) +1]) // 开辟s._str大小的空间,+1是因为‘/0’
//{
// strcpy(_str, s._str);
//}
//
//String& String::operator=(const String& s)
//{
// if (this != &s) // 判断是否相同
// {
// if(_str)
// delete[] _str; // _str不为空,则需要先释放空间
// _str = new char[strlen(s._str) + 1];
// strcpy(_str, s._str);
// }
// return *this;
//}
//
//String::~String()
//{
// if (NULL != _str)
// {
// delete[] _str;
// }
//}
//现代写法完成String深拷贝
String::String(const char* str)
: _str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}
String::String(const String& s)
{
String tmp(s._str); // 用s._str定义一个tmp对象
swap(_str, tmp._str); // 将指针进行交换
}
String& String::operator=(String s) // 形参调用构造函数
{
swap(_str, s._str);
return *this;
}
String::~String()
{
if (NULL != _str)
{
delete[] _str;
}
}
相比较两种方法而言,更推荐现代写法,原因如下:
- 现代写法代码量相对而言较少,效率更高
- 现代写法的赋值运算符重载中调用了拷贝构造、拷贝构造中调用了构造函数,故现代写法中的复用性更强,更易于管理
-
传统写法中的赋值运算符重载会先释放原先开辟的空间,这样如果下面重新开辟空间失败不能拷贝时,那么原有数据不会被保留
深拷贝虽然会再次开辟空间增加内存的占用,但这样可以解决浅拷贝不能做到的问题。
写时拷贝
其实通过上面两种拷贝方式就可以解决很多问题,但浅拷贝不能修改,深拷贝如果不修改又占有了更多空间。因此写时拷贝就出现了,其实写时拷贝就是将上面两种方法的优点进行结合,通过引入一个引用计数来解决这些问题。
写时拷贝较上面方法更为复杂,所以在这里先进行说明:写时拷贝需要一个引用计数,因此要增加一个存放一个引用计数的位置,我们将这个位置放在_str前面的四个字节,每当拷贝一次就将引用计数++一次,进行修改时先判断一下引用计数是否为1,若为1则直接进行修改;否则先重新开辟一块空间,将原有引用计数–,再进行修改。
下面简单编写一下其代码:
class String
{
public:
String(char* str = "")
:_str(new char[(strlen(str) + 5)]) // 多开辟4个字节来存放引用计数
{
*((int*)_str) = 1;
_str += 4; // str中内容在引用计数后
strcpy(_str, str);
}
String(const String& s)
{
_str = s._str; // 基本与浅拷贝相似
++GetRefCount();
}
String& operator=(const String& s)
{
if (_str != s._str)
{
if (--(GetRefCount()) == 0)
{
delete[] _str;
}
_str = s._str;
++(GetRefCount());
}
return *this;
}
~String()
{
if (--(GetRefCount()) == 0)
{
delete[] (_str - 4);
}
}
int& GetRefCount() // 返回引用类型,也可写成指针类型
{
return *((int*)(_str - 4));
}
private:
char* _str;
};
写时拷贝基本上解决了浅拷贝与深拷贝中所存在的问题,在单线程中写时拷贝不会出现问题,但在多线程中也会有一些问题。
转载自原文链接, 如需删除请联系管理员。
原文链接:C++中的拷贝方式(string),转载请注明来源!