c++---了解c++中浅拷贝,深拷贝,引用计数,写时拷贝以及c++中string类的模拟实现

以下我们都用string类来说明。

一. 浅拷贝

   首先我们来说一说浅拷贝,浅拷贝是什么呢?浅拷贝就是指将拷贝时只将指针拷贝过来了,和被拷贝的内容指向的是同一块空间。这样就会出现不想要的结果。

  1. 当有一个指针想要对其这块空间进行修改,那么这时,并不是只有一个指针指向这里,而还有别的指针。所以这里是一块共享的内存空间,那么有其中一个对其改变,其余指针的内容也将受到影响,这样就会影响其它的工作。
  2. 如果是在对象之间发生这样的事情,有两个对象的成员变量都指向同一块内存空间,不仅会发生上面那样的事,这块空间在析构时,可能会被释放多次。就会导致程序出错。

就像下面这个例子一样:
是直接用被拷贝的字符串指针初始化拷贝的字符串。

  String(const String& s)
        :_str(s._str)
    {}

浅拷贝

二. 深拷贝

   深拷贝就是和浅拷贝对应的一种拷贝方式,深拷贝指的是将要被拷贝的对象中的所有成员变量都拷贝一份,重新开辟一块内存空间,在这块内存上存着,自己指向自己所对应的内存空间,等到最后需要析构时,也是自己释放自己的空间,不会出现多次释放同一块空间的情况,也不会出现改了一块空间,其余的对象受影响的情况。这样的两个对象是相互独立的,只是值相同而已。
深拷贝

String(const String& s)
    :_str(new char[s._size + 1])
    ,_size(s._size)
    ,_capacity(s._capacity)
{
    strcpy(_str,s._str);
}

三. 引用计数

   引用计数与写时拷贝是对深拷贝与浅拷贝的结合。为了不浪费空间,并且可以对所指的内容更改,就产生了这样的一种方法。
   这次我们就可以先采用浅拷贝,若是暂时不需要对这些变量进行改变。而且需要它的值与其它的值相同,就可以给这些对象只开辟一块内存空间,让其共享。不允许对共享的区域改变。也不可以释放这块空间。所以,引入了引用计数,在其共享区域多开辟一个字节的空间,用于计数,若是超过一个对象使用,就不可以随意更改以及释放。若是只有一个对象使用,就可以更改释放。
   这里有两种引用计数的方法。

  1. 将计数的内存与对象的成员变量分开开辟,但是这样就会多开辟一次空间,会有一定的成本。

①
代码如下:

class String 
{ 
public: 
String(const char* str = "")
   :_str(new char[strlen(str)+1])
    ,_pCount(new size_t(1))
    {
        strcpy(_str,str);
    }
// s2(s1) 
 String(const String& s)
     :_str(s._str)
     ,_pCount(s._pCount)
{
    (*_pCount)++;
}
 //s2 = s1 
 String& operator=(const String& s)
{
    if(s._str != _str)
    {
        if(*_pCount == 1)
        {
            delete[] _str;
        }
        else
        {
            (*_pCount)--;
        }
            _str = s._str;
            _pCount = s._pCount;
            (*_pCount)++;
    }
        return *this;
}
 ~String()
{
    if(*_pCount == 1)
    {
        *(_pCount) = 0;
        delete[] _str; 
        delete _pCount;
    }
}
 const char* c_str()
{
    char *ret ;
    ret = new char[strlen(_str)]+1;
    ret = strcpy(ret,_str);
    return ret;
}
 char& operator[](size_t pos)
{
    assert(pos>strlen(_str));
    return _str[pos];
}

 private: 
 char* _str; 
 size_t _pCount;
  1. 将其一起开辟,在开辟的空间的开始四个字节用于计数。
    ②
 class String 
 { 
 public: 
 String(const char* str = "")
     :_str(new char[strlen(str)+5])
 {
     *(int*)(_str)=1;
     _str = _str + 4;
     strcpy(_str,str);
 }
 // s2(s1) 
 String(const String& s) 
 {
     _str = s._str;
    (*(int*)(_str-4))++;
 }
 //s2 = s1 
 String& operator=(const String& s)
 {
     if(s._str != _str)
     { 
        if(*(int *)(_str - 4) == 1)
            delete[] (_str - 4);
        _str = s._str;
        *(int *)(_str - 4) += 1;
     }  
        return *this;
 }
 ~String() 
 {
     if(*(int *)(_str - 4) == 0)
         delete[] (_str-4);
 }
 const char* c_str()
 {
     char* ret = new char[strlen(_str)+1];
     strcpy(ret,_str);
     return ret;
 }

 char& operator[](size_t pos)
 {
     return _str[pos];
 }

 private: 
 char* _str; // 引用计数在头上 
 }; 

四. 写时拷贝

   上面说了,不允许在共享的区域对公共部分改变以及释放。这不代表着创建了对象就不可以对其再改变了,我们可以在需要对它进行改变时将它拷贝出来,就相当与一次深拷贝。再对它改变。由于有两种的计数方式,所以也就是有两种的写时拷贝。
第一种:

 void CopyOnWrite()
{
    char *ret = new char[strlen(_str)+1];
    strcpy(ret,_str);
    _str = ret;
    (*_pCount)--;
    *_pCount = 1;
}

第二种:

 void CopyOnWrite()
 {
     char *tmp = new char[strlen(_str)+5];
     *((int*)tmp) = 1;
     *((int *)(_str)-1) -= 1;
     tmp += 4;
     strcpy(tmp,_str);
     _str = tmp;
 }

总结:这种方法既可以满足我们需要的功能,也能一定程度上节省空间。最坏的情况也就是全部进行深拷贝,和深拷贝的代价一样。但是不可能每次都是最坏的情况。所以,引用计数与写时拷贝是一种很有效的方法。这种方法在其它地方也有应用。比如linux操作系统中父子进程之间的关系。

阅读更多

没有更多推荐了,返回首页