什么是浅拷贝?
浅拷贝就是拷贝指向对象的指针,浅拷贝又称位拷贝,意思就是说:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间,浅拷贝只是一种简单的拷贝,让几个对象共用一个内存,然而当内存销毁的时候,指向这个内存空间的所有指针需要重新定义,不然会造成野指针错误。
第一种方法:深拷贝
深拷贝就是: 给每个对象分配独立的内存资源,保证多个对象之间不会因为共享资源时,多次释放造成程序崩溃问题。
简单来说,就是增加了一个指针,并新开辟了一块空间,让指针指向这块新开辟的空间。
用深拷贝可以有两种方法实现:
1. 传统方法:
构造函数和析构函数较为简单和浅拷贝实现相同,在拷贝构造函数为对象开辟新的空间资源,在赋值运算符重载函数中需要注意在赋值时对原空间的释放,防止内存泄漏。
下面给出代码:
在实现时将类的实现放入命名空间bit1中,防止与标准库的string类发生冲突
namespace bit1
{
class string
{
public:
//构造函数
string(const char* s = "")
{
if (nullptr == s)//预防空指针的特殊情况
s = "";
_str = new char[strlen(s) + 1];
strcpy(_str, s);
}
//拷贝构造函数
string(const string& s)
:_str(new char[strlen(s._str)+1])//为新对象开辟新的内存资源
{
strcpy(_str, s._str);
}
//赋值运算符重载函数
string& operator=(const string& s)
{
if (&s != this)
{
char* ptr = new char[strlen(s._str)+1];//这里利用中间量,防止未申请到空间
strcpy(ptr, s._str);
delete[] _str;//释放旧空间资源,防止内存泄漏
_str = ptr;
}
return *this;
}
//析构函数
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
}
2. 现代版简洁方法:
现代版的方法虽然简洁,但是理解起来有难度,细节较多。
这种写法有个特点就是:调用其他的函数来实现自己的功能。所以在实现时要细心。
还有一个重要的操作就是:交换空间。
实现方法如下:
namespace bit2
{
class string
{
public:
string(const char* str = "")//和传统方法相同
{
if (str == nullptr)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
//拷贝构造函数
string(const string& s) :_str(nullptr)
{
string temp(s._str);//函数调用结束时会被析构
swap(_str, temp._str);//交换
}
//赋值运算符重载
string& operator=(string s)// s对象此时为临时拷贝的一份资源,函数返回时会析构掉
{
if (this != &s)
{
swap(_str, s._str);//交换
}
return *this;
}
~string()//和传统方法相同
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
}
深拷贝的传统写法和现代写法比较:
传统写法:可读性高,便于理解,但操作性较低。
现代写法:代码更加简洁高效,但是逻辑更加复杂。
第二种方法:写时拷贝(COPY-ON-WRITE)
写时拷贝:浅拷贝+计数引用+修改对象时分离对象;
写时拷贝顾名思义就是在写的时候(即改变字符串的时候)才会真正的开辟空间拷贝(深拷贝),如果只是对数据的读时,只会对数据进行浅拷贝。
写时拷贝使用一个计数器来计数浅拷贝对象个数,在需要修改对象是将该对象从浅拷贝“群”中分离出来,为该对象分配空间,增加新计数器,将源计数器做相应的修改。所以,在实现时,要注意计数器的及时更新以及分离对象分配内存的时机。
namespace bit3
{
class string
{
public:
//构造函数
string(const char* str = "")
{
if (str == nullptr)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
_pCount = new int(1);
}
//拷贝构造函数
string(const string& s)
:_str(s._str)
, _pCount(s._pCount)
{
strcpy(_str, s._str);
memcpy(_pCount, s._pCount, sizeof(_pCount));
++(*_pCount);
}
//赋值运算符的重载
string& operator=(const string& s)
{
if (this != &s)
{
Release();
_str = s._str;
_pCount = s._pCount;
++(*_pCount);
}
return *this;
}
//析构函数
~string()
{
Release();
//保证即使不是最后一个共用对象也指向空
_str = nullptr;
_pCount = nullptr;
}
char& operator[](size_t index)
{
//在修改对象内容时,要防止一改全改的情况,所以,此时要分离对象
if (*_pCount > 1)
{
string temp(_str);
Swap(temp);
//此时对象已被分离,temp替代对象的原位置,在函数返回时,会调用temp的析构函数,将计数器减1
}
return _str[index];
}
void Swap(string& temp)
{
swap(_str, temp._str);
swap(_str, temp._str);
}
const char& operator[](size_t index)const
{
return _str[index];
}
private:
//判断是否为对象独占资源,若是销毁资源,若不是,不做处理
void Release()
{
//该方式会有线程安全问题
if (--(*_pCount) == 0)
{
delete[] _str;
delete _pCount;
_str = nullptr;
_pCount = nullptr;
}
}
private:
char* _str;
int* _pCount;
};
}
写时拷贝比深拷贝的效率高,写时拷贝同时了具备浅拷贝和深拷贝的优点。
但是这种方法还是有点缺陷,那就是没有考虑线程安全问题。但博主现在能力还不足以完美的改进该代码,望见谅!
keep Running