深浅拷贝以及引用计数

来源:互联网 发布:matlab 最优化算法 书 编辑:程序博客网 时间:2024/06/06 20:44

标准库类型string表示可变长的字符序列,使用string类型必须首先包含它的头文件。
作为标准库的一部分,string定义在命名空间std中。

#include<string>//注意这里没有.husing namespace std;

下面让我们一起来模拟string类中的几个比较重要的成员函数,一步一步的剖析它们的实现机制:

1.构造函数:

示例①

class String{public :    String(char *str = "")//construction function        :_str(str)    {}    ~String()    {        if (NULL != _str)        {            delete[] _str;        }    }private :    char *_str;};void Test2(){    String s1("hello");    String s2(new char[3]);}

这里写图片描述

从监视窗口可以看出,s1貌似构建成功了,但是在Test2()快要结束时,程序崩溃了,我们知道,析构函数是在对象销毁前执行的,
所以程序崩溃的原因就是执行析构函数的时候,析构函数中释放对象中指针_str指向的空间,
但是S1中指针_str指向的是一个常量字符串—->存放在代码区(c语言)或者说常量区(操作系统),而delete[]释放的空间必须要是动态分配的,至此我们就可知程序崩溃的原因。

经过上例的讨论,我将构造函数做了些许的修改:
示例②:

class String{public :    String(char *str = "")//构造函数    {        if (NULL == str)        {            _str = new char[1];//为了和delete[]配合使用            *_str = '\0';        }        else        {            _str = new char[strlen(str) + 1];            strcpy(_str, str);        }    }    ~String()    {        if (NULL != _str)        {            delete[] _str;        }    }private :    char *_str;};

构造函数的参数列表带有默认值也是为了和类库中的一致,当以string s1;形式构造对象时,希望构造出的字符串为空串(含有’\0’)。

2.拷贝构造函数:

①浅拷贝:

String::String(const String& s)//浅拷贝:_str(s._str){}

测试:

void Test2(){    String s1("hello");    String s2(s1);}

上面的程序会崩溃,下面我们来分析原因:
这里写图片描述

可以看出这里的s2采用了浅拷贝,而析构的时候,先析构的是s2,析构s2时会释放掉“hello”这块空间,接下来再释放s1时,程序就会崩溃,因为这块空间被释放了两次。

浅拷贝:又称为位拷贝,编译器只是将指针的内容拷贝过来,导致多个对象共用一块内存空间,当其中任意对象将这块空间释放之后,另外一些对象并不知道这块空间已经还给了操作系统,以为还有效,所以再对这块空间进行操作时,造成了违规访问。

为了解决这个问题,我们引入了深拷贝(给要拷贝构造的对象重新分配空间):

这里写图片描述

代码如下:

String::String(const String& s)//深拷贝:_str(new char[strlen(s._str) + 1]){    strcpy(_str, s._str);}

测试:

void Test2(){    String s1("hello");    String s2(s1);}

这里写图片描述

由监视窗口可知,拷贝的对象s2中_str的值(字符串的地址)和s1对象中的_str的值不同,说明是重新开辟了空间(即就是深拷贝)。

3.赋值运算符重载:

//方法①String& String::operator=(const String& s){    if (this != &s)    {        delete[] _str;        _str = new char[strlen(s._str) + 1];        strcpy(_str, s._str);    }    return *this;//为了支持链式访问}//方法②(优)String& 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;//为了支持链式访问}

测试:
这里写图片描述

一般情况下,上面的两种写法都可以,但是相对而言,第二种更优一点。
对于第一种,先释放了旧空间,但是如果下面用new开辟新空间时有可能失败——>抛异常,而这时你是将s2赋值给s3,不仅没有赋值成功(空间开辟失败),而且也破坏了原有的s3对象。

对于第二种,先开辟新空间,将新空间的地址赋给一个临时变量,就算这时空间开辟失败,也不会影响原本s3对象。

综上:第二种方法更优一点。

最后的返回值是为了支持链式访问。
例如:s3 = s2 = s1;

上面所写的拷贝构造函数和赋值运算符重载函数属于传统写法,下面我们一起来看看它们的现代写法:

拷贝构造的现代写法:
这里写图片描述

赋值运算符重载函数的两种现代写法:

这里写图片描述
在面试时,一般写出上面四个string类的成员函数即可,除非面试官特别要求。

从上面的深拷贝我们可以看出,相比浅拷贝,深拷贝的效率明显较低,因为每拷贝一个对象就需要开辟空间和释放空间,再有就是赋值运算符重载也是一样的需要重新开辟空间并释放空间。
假如有这样的一种情况,拷贝和赋值得到的对象只用于”读”,而不用于”写”,那么是不是就不需要重新开辟空间了呢?

下面让我们一起来了解一下引用计数的浅拷贝:

我们需要使用一个变量可以标记同一块空间同时有几个对象在使用—–>为了析构(当标记的这个变量为1时,说明只有一个对象在使用这块空间,那么在使用完成后就可以去释放空间,否则让这个计数的变量减1)。
引用计数的浅拷贝:

首先,我们需要讨论下这个所谓的计数变量:
①使用数据成员 int _count;
在构造函数中使得_count = 1;
在拷贝构造函数中_count(++s._count).
但是析构的时候无法改变每个对象中的_count的值。

String.h

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>using namespace std;class String{public:    String(const char* str = "");    String(String& s);    String& operator=(const String& s);    ~String();private:    int& GetRef(char *str);    void release();    char *_str;    int _count;};

String.cpp

String::String(const char* str ){    if (NULL == str)    {        _str = new char[1];        *_str = '\0';    }    else    {        _str = new char[strlen(str) + 1];        strcpy(_str, str);    }    _count = 1;}String::String(String& s):_str(s._str), _count(++(s._count)){    strcpy(_str, s._str);}String::~String(){    if (--_count == 0 && NULL != _str)//_count是每个对象的,减去一个共享一块空间的其中一个对象的成员,并不会改变其他对象的成员变量_count的值    {        cout << this << endl;        delete[] _str;    }}

所以,使用数据成员变量—–>pass

②使用静态数据成员:static int _count;
构造函数内:_count =1;
拷贝函数内部:_count++;
当出现情况:String s1;
String s2(s1);
String s3(s2);
这时的引用计数_count = 3;
但是当重新构造一个对象时:
String s4;——–>使得该类所有的引用计数_count = 1;

所以:静态数据成员也不可以。
下面详细讲解:

String.h

#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>using namespace std;class String{public:    String(const char* str = "");    String(String& s);    String& operator=(const String& s);    ~String();private:    int& GetRef(char *str);    void release();    char *_str;    static int _count;};

String.cpp

String::String(const char* str ){    if (NULL == str)    {        _str = new char[1];        *_str = '\0';    }    else    {        _str = new char[strlen(str) + 1];        strcpy(_str, str);    }    _count = 1;}String::String(String& s):_str(s._str){    strcpy(_str, s._str);    _count++;}String::~String(){    if (--_count == 0 && NULL != _str)    {        cout << this << endl;        delete[] _str;    }}int String:: _count = 0;void Test1(){    String s1("hello");    String s2(s1);    String s3(s2);}

这里写图片描述

这里写图片描述

从上图可以看出静态成员变量是这个类所有的对象所共享的,所以当只是用一个对象去拷贝另一个对象时,这样不会出错,而且貌似也达到我们的要求了,可是当重新构造一个对象时,就会发现所有对象中的_count都修改为了1。

所以,使用静态成员(static int count)————>pass

③使用指针:
使得拷贝的对象共用指向一个引用计数。
这里写图片描述

赋值运算符重载函数中的两种情况:
第一种情况:
这里写图片描述

这里写图片描述

//String.h#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>using namespace std;class String{public:    String(const char* str = "");    String(String& s);    String& operator=(const String& s);    ~String();private:    int& GetRef(char *str);    void release();    char *_str;    int *_RefCount;};
//String.cpp#include"String-Ref.h"String::String(const char* str ){    if (NULL == str)    {        _str = new char[1];        *_str = '\0';    }    else    {        _str = new char[strlen(str) + 1];        strcpy(_str, str);    }    _RefCount = new int(1);}String::String(String& s):_str(s._str),_RefCount(s._RefCount){    strcpy(_str, s._str);    (*_RefCount)++;}String::~String(){    if (--(*_RefCount) == 0 && NULL != _str)    {        cout << this << endl;        delete[] _str;        _str = NULL;        delete _RefCount;        _RefCount = NULL;    }}String& String::operator=(String& s){    if (this != &s)    {        if (--*_RefCount == 0)        {            delete[] _str;            _str = NULL;            delete _RefCount;            _RefCount = NULL;        }        _str = s._str;        _RefCount = s._RefCount;        (*s._RefCount)++;    }    return *this;}void Test1(){    String s1("hello");    String s2(s1);    String s3(s2);    String s4("world");    s4 = s2;}

这里写图片描述

这里写图片描述

由监视窗口的数据可得,使用指针可以完成引用计数的浅拷贝,但是因为每构造一个对象,都需要开辟两块空间,这样容易造成内存碎片。

由new[]可以联想到类似模型—->只开辟一块空间(多开四个字节),把引用计数放在字符串首地址的前四个字节上。
这样不但解决了内存碎片问题,而且也可以程序的运行效率。

引用计数的浅拷贝的最优解决方案:
(这里我就不一一分析了,思路和上面使用指针是一样的)

这里写图片描述

//String.h#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>using namespace std;class String{public :    String(const char* str = "");    String(const String& s);    String& operator=(const String& s);    ~String();private :    int& GetRef(char *str);    void release();    char *_str;};
//String.cpp#include"string_simi.h"String::String(const char* str)//构造函数{    if (NULL == str)    {        _str = new char[5];        _str += 4;        *_str = '\0';    }    else    {        _str = new char[strlen(str) + 5];        _str += 4;        strcpy(_str, str);    }    GetRef(_str) = 1;}String::String(const String& s)//拷贝构造函数:_str(s._str){    GetRef(_str)++;}//s1 = s2;String& String::operator=(const String& s)//赋值运算符重载{    if (_str != s._str)//判断自赋值    {        //1.当s1的引用计数为1时        //①释放空间        //②改变s1指针的指向        //③s2的引用计数加1        release();        //当s1的引用计数大于1时        //①s1的引用计数减1        //同上②③        _str = s._str;        GetRef(s._str)++;    }    return *this;}void String::release(){    if (--GetRef(_str) == 0)    {        delete[](_str - 4);    }}int& String::GetRef(char *str)//获取引用计数{    return *(int*)(str - 4);}String::~String()//析构函数{    if (--GetRef(_str) == 0)    {        delete[] (_str - 4);    }}void TestString1(){    String s1("hello");    String s2(s1);    String s3("world");    s1 = s3;}int main(){    TestString1();    return 0;

引用计数的浅拷贝同样存在缺陷—>当几个共用同一块空间的对象中的任一对象修改字符串中的值,则会导致所有共用这块空间的对象中的内容被破坏掉。

由此—->引入了写时拷贝(Copy On Write)
顾名思义,写时拷贝—>写的时候进行拷贝(深拷贝)

//String.h#pragma once#include<iostream>using namespace std;#include<assert.h>class String{public:     String(const char* str = "")//构造函数     {          if (NULL == str)          {              char* pTemp = new char[1 + 4];              _pStr = pTemp + 4;              _GetRefer() = 1;              *_pStr = '\0';          }          else          {              char* pTemp = new char[strlen(str) + 1 + 4];              _pStr = pTemp + 4;              strcpy(_pStr, str);              _GetRefer() = 1;          }     }     String(const String& s)          :_pStr(s._pStr)     {          _GetRefer()++;     }     ~String()     {          Release();     }     String& operator=(const String& s);     char& operator[](size_t index);     const char& operator[](size_t index)const;     int& String::_GetRefer();     void Release();     friend ostream& operator<<(ostream& _cout, const String& s);private:     char* _pStr;};
//String.cpp#define _CRT_SECURE_NO_WARNINGS 1#include"String3.h"int& String::_GetRefer()//获取引用计数{     return *((int*)_pStr - 1);}void String::Release()//释放空间{     if (--_GetRefer() == 0)     {          _pStr -= 4;          delete[] _pStr;          _pStr = NULL;     }}String& String::operator=(const String& s){     if (_pStr != s._pStr)     {          if (--_GetRefer() == 0)//需要考虑两种情况          {              Release();              _pStr = s._pStr;              _GetRefer()++;          }          else          {              _pStr = s._pStr;              _GetRefer()++;          }     }     return *this;}char& String::operator[](size_t index){     if (_GetRefer() > 1)     {          _GetRefer()--;          char* pTemp = new char[strlen(_pStr) + 1 + 4];          pTemp += 4;          strcpy(pTemp, _pStr);          _pStr = pTemp;          _GetRefer() = 1;//新开的空间     }     return _pStr[index];}const char& String::operator[](size_t index)const{     return  _pStr[index];}void Test1(){     const String s1("hello");     String s2(s1);     //String s3;     //s3 = s2;     cout << s1[4] << endl;}int main(){     Test1();     return 0;}
[]重载运算符需要成对重载:char& String::operator[](size_t index){     if (_GetRefer() > 1)     {          char* pTemp = new char[strlen(_pStr) + 1 + 4];          pTemp += 4;          strcpy(pTemp, _pStr);          _GetRefer()--;          _pStr = pTemp;          _GetRefer() = 1;//新开的空间     }     return _pStr[index];}const char& String::operator[](size_t index)const{     return  _pStr[index];}
2 0