浅拷贝
什么是浅拷贝与其危害
浅拷贝也称位拷贝,指的是编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规。
举个例子:
假设被拷贝过来的值有指针,而这个指针指向的是一块正在使用的空间,那么当拷贝结束后,就有两个对象同时管理这块空间了,这显然是不合理的, 当一个对象释放资源的时候,那么另一个对象如果继续操作这块空间,肯定会出问题的。
如图所示:
注意:编译器默认的拷贝构造函数是浅拷贝,通常我们需要手动实现深拷贝。
如何解决浅拷贝问题
解决浅拷贝问题可以通过深拷贝,或者使用 引用计数的写时拷贝 来完成。
引用计数的写时拷贝
知道硬链接的朋友可能容易理解点。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
深拷贝
什么是深拷贝
深拷贝的出现就是为了解决浅拷贝不能完成的工作,如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
深拷贝的实现
深拷贝会给每个对象独立分配资源,保证资源使用不冲突。深拷贝有两种不同的实现方式,一种是传统写法,一种是现代写法。 两者差异体现在过程的繁琐与否上,下面通过string的拷贝构造和赋值运算符重载来演示 传统写法与现代写法的差异。
传统写法
特点:可读性强但代码相对繁琐。(自己当工人)
string的传统写法
#include <string.h>
namespace mystring {
class string
{
public:
string(const char* s = "") {
if (nullptr == s) {
assert(false);
return;
}
_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 (this != &s) {
char* tmp = new char[strlen(s._str) + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
}
return *this;
}
~string() {
delete[] _str;
_str = nullptr;
}
private:
char* _str;
};
现代写法
特点:相对抽象但代码简洁巧妙。(找工人干活swap)
string的现代写法
namespace mystring2 {
class string {
public:
string(const char* s = "") {
if (s == nullptr) {
s = "";
}
_str = new char[strlen(s)];
strcpy(_str, s);
}
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);//调用了string(const char* s = "") 复用代码
swap(_str, tmp);
}
string& operator=(const string& s) {
if (this != &s) {
swap(_str, s._str);
}
return *this;
}
~string() {
if (_str != nullptr) {
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
void test1() {
s1 = "hello";
s2 = "";
s2 = s1; //调用拷贝构造,再调用赋值运算符的重载 传参进去的时候拷贝一次 紧接着交换临时变量与原来对象指向的空间
}
}