深浅拷贝以及引用计数

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

#include<string>//注意这里没有.h
using 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
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值