String类的拷贝(浅拷贝,深拷贝,写时拷贝)

时间:2020-12-21 19:51:52
浅拷贝:也称位拷贝,编译器只是直接将指针的值拷贝过来,结果多个对象共用同一块内存,当一个对象将这块内存释放掉之后,另一些对象不知道该块空间已经还给了系统,以为还有效,所以在对这段内存进行操作的时候,发生了访问违规。

浅拷贝使多个对象共用一块内存地址,调用析构函数时导致一块内存被多次释放,导致程序崩溃。

存在问题:
假如有一个成员变量的指针,char *m_data;
其一,浅拷贝只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时,
会造成同一份资源析构2次,即delete同一块内存2次,造成程序崩溃。
其二,浅拷贝使得obj2.m_data和obj1.m_data指向同一块内存,任何一方的变动都会影响到另一方。

其三,在释放内存的时候,会造成obj1.m_data原有的内存没有被释放,造成内存泄露。

浅拷贝代码:

class String
{
public:
String(const char* pStr = "") //构造函数
{
if(NULL == *pStr)
{
_pStr = new char[1];
_pStr = '\0';
}

else
{
_pStr = new char(strlen(pStr)+1);
strcpy(_pStr, pStr);
}
}

String(const String& s) //拷贝构造函数
{

_pStr = new char[strlen(s._pStr)+1];
strcpy(_pStr, s._pStr);

}

String& operator = (const String &s) //赋值运算符
{
if(this != &s) //判断是不是给自己赋值
{
_pStr = new char[strlen(s._pStr)+1];
strcpy(_pStr, s._pStr);
}
return *this;
}

~String() //析构函数
{
delete [] _pStr;
_pStr = NULL;
}

private:
char* _pStr;
};

为了解决浅拷贝中出现的问题,引入了深拷贝
深拷贝:

普通版代码:

class String
{
public:
String(const char * pStr = " ")
{
if(NULL == *pStr)
{
_pStr = new char[1];
_pStr = '\0';
}
else
{
_pStr = new char[strlen(pStr)+1];
strcpy(_pStr, pStr);
}
}

String(const String& s)
:_pStr(new char[strlen(s._pStr)+1])
{
strcpy(_pStr, s._pStr);
}

String operator = (const String &s)
{
if(this != &s)
{
String temp = new char[strlen(s._pStr)+1]; //采用临时变量对对象进行赋值操作,避免二次析构
strcpy(temp._pStr, s._pStr);
return temp;
}
}~String(){delete[] _pStr;_pStr = NULL;}private:char* _pStr;};

深拷贝(简洁版):

class String
{
public:
String(const char * pStr = " ")
{
if(NULL == *pStr)
{
_pStr = new char[1];
_pStr = '\0';
}

else
{
_pStr = new char[strlen(pStr)+1];
strcpy(_pStr, pStr);
}
}

String(const String& s)
:_pStr(NULL) //先給_pStr初始化成NULL,以免后续出现乱码的情况,否则在进行后续交换的时候,临时对象
{//里的_pStr就会变成乱码,在最后进行析构函数调用的时候就无法析构
String pTemp(s._pStr); //临时变量了

std::swap(_pStr, pTemp._pStr);
}

String& operator = (const String &s)
{
if(this != &s)
{
String str(s);
std::swap(_pStr, str._pStr);
}

return *this;
}

~String()
{
delete[] _pStr;
_pStr = NULL;
}

private:
char* _pStr;
};
引用计数实现拷贝:

代码实现:

class String
{
public:
String( char* pStr = "")
:_pCount(new int(1))
{
if(NULL == *pStr)
{
_pStr = new char[1];
_pStr = '\0';
}
else
{
_pStr = new char[strlen(pStr)+1];
strcpy(_pStr, pStr);
}
}

String(const String& s) //当每次都共用同一块内存空间时,计数器要进行加一的计数
:_pStr(s._pStr)
,_pCount(s._pCount)
{
++(*_pCount);
}

String & operator = (const String &s)
{
if(_pStr != s._pStr)
{
if(--(*_pCount) == 0) //当计数器等于0的时候,就要释放旧空间,申请新空间,计数器加一
{
delete[] _pStr;
delete _pCount;
_pStr = NULL;
_pCount = NULL;
}

_pStr = s._pStr;
_pCount = s._pCount;
(*_pCount)++;

}
return *this;
}

~String()
{
if(--(*_pCount)==0 && _pStr) //计数器为0时,释放该空间
{
delete[] _pStr;
delete _pCount;
_pStr = NULL;
_pCount = NULL;
}
}

private:
char* _pStr;
int* _pCount;

};
写时拷贝:

在复制一个对象的时候并不是真正的把原先的对象复制到内存的另外一个位置上,而是在新对象的内存映射表中设置一个指针,指向源对象的位置,并把那块内存的Copy-On-Write位设置为1.

这样,在对新的对象执行读操作的时候,内存数据不发生任何变动,直接执行读操作;而在对新的对象执行写操作时,将真正的对象复制到新的内存地址中,并修改新对象的内存映射表指向这个新的位置,并在新的内存位置上执行写操作。

这个技术需要跟虚拟内存和分页同时使用,好处就是在执行复制操作时因为不是真正的内存复制,而只是建立了一个指针,因而大大提高效率。但这不是一直成立的,如果在复制新对象之后,大部分对象都还需要继续进行写操作会产生大量的分页错误,得不偿失。所以COW高效的情况只是在复制新对象之后,在一小部分的内存分页上进行写操作。


class String
{
public:

String(char *str = "")
:_pStr(new char[strlen(str) + 1 + 4])
{
*(int *)_pStr = 1;
_pStr += 4;
strcpy(_pStr, str);
}

String(const String& s)
:_pStr(s._pStr)
{
++GetCount();
}

String & operator = (const String &s)
{
if(this != &s)
{
if(--GetCount() == 0 && _pStr)
{
delete[] (_pStr - 4);
}
_pStr = s._pStr;
++GetCount();
}
return *this;
}

~String()
{
if (--GetCount() == 0 && _pStr)
{
delete[] (_pStr - 4);
}
}

char& operator[](int index)//写时拷贝
{

if (GetCount() > 1)//当引用次数大于1时新开辟内存空间
{
--GetCount();//原来的空间引用计数器减1
char *pStr = new char[strlen(_pStr) + 1 + 4];
strcpy(pStr, _pStr);
_pStr = pStr;
++GetCount();
}
return *(_pStr+index);;
}


friend ostream& operator <<(ostream& _cout, const String &s);

private:
int& GetCount()
{
return *((int *)_pStr-1);
}

private:
char* _pStr;


};


ostream& operator <<(ostream& _cout, const String &s)
{
_cout<<s._pStr;
return _cout;
}