之前的博客已经给出了如何自己定义一个string类,以及其内部应该有的操作,今天就让我们根据STL库中给出的string来看看,它重要的写实拷贝实现和一点点读时拷贝(也是写时拷贝)


1、写时拷贝(copy-on-write)

class String
{
 public:
 String(const String &str)
 :_pData(NULL)
 {
     String temp(str._pData);
     swap(_pData,temp._pData);
 }
 private:
 char *_pData;
}
void test()
{
    String s1("hello world");//构造
    String s2(s1);//拷贝构造
}

这里面实现的是用空间换时间的一种方法,定义极其简单,然而大神们写出来的STL库中的string是更为精巧的。就是应用了写实拷贝的技术,防止浅拷贝发生,并且还省了空间,j_0069.gif

那么问题来了????

Q:什么是写时拷贝呢?

A:写时拷贝就是一种拖延战术,当你真正用到的时候才去给它开辟空间,不然它只是看起来存在,实际上只是逻辑上的存在,这种方法在STL的string中体现的很明显。

由于string类是用char* 实现的,其内存都是在堆上开辟和释放的。堆上的空间利用要很小心,所以当你定义一个sring类的对象,并且想对这个对象求地址,其返回值是const char*类型,onlyread属性哦,如果还想对该地址的内容做什么改变,只能通过string给的方法去修改。

举个栗子:

#include 
#include 
using namespace std;
int main()
{
    string s1("再来一遍:hello world");
    string s2(s1);
    //c++方式打印一个字符串的地址!!!!!
    //static_cast---c++中的强制类型转换,不检查
    //string的c_str()方法返回值是const char *
    cout<(s1.c_str())<(s2.c_str())<_<)~~~~我也这么觉得
    printf("%x\n",s1.c_str());
    printf("%x\n",s2.c_str());
}

(vs2010版本)

结果是不是和你想的不一样。。。。(明明应该不变的说~)

vc 6.0版本下:s1,s2的地址是一样的。这里就不进行截屏了,如果有兴趣的同学,下去可以试试哈~

那么当对s1,s2进行修改时是怎么样的呢

s1[0]='h';
s2[0]='w';

(vs2010版本)

VC6.0版本下:s1,s2的地址不一样(同vs2010版本)

所以我们得出的结论是:

当对string对象只进行拷贝构造时,发生的是写时拷贝(假拷贝),只有对其对象进行修改时(有写的操作),才对其对象另外开辟空间,进行修改。

要想达到这样的效果,在一定程度上节省了空间。

必须做到两点:内存的共享,写时拷贝。


(1)copy-on-write的原理?

         “引用计数”,程序猿就是这般机智~~~~

         当对象s1实例化,调用构造,引用计数初始化=1;

         当有对象对s1进行拷贝时,s1的引用计数+1;

         当有对象是由s1拷贝来的或者是s1自身进行析构是,s1的引用计数进行-1;

         当有对象是由s1拷贝来的或者是s1自身需要修改时,进行真拷贝,并且引用计数-1;

         当引用计数==0的时候,进行真正的析构。


(2)引用计数应该如何设计在?

            关于引用计数的实现,你是不是也有这样的疑惑呢?

            当类的对象之间进行共享时,引用计数也是共享的

            当类中的对象从公共中脱离出来,引用计数就是它自己的了。

            那么如何做到从独立--->共享--->独立的呢???

            如果你想将引用计数当做String类的成员变量,那么什么样的类型适合它呢?

             int _count;  那么每个对象的实例化都拥有一个自己的引用计数,无法实现共享

class String
{
    public:
        String(pData=NULL)
        :_pData(new char[strlen(pData)+1])
        ,_count(1)
        {
            strcpy(_pData,pData);
        }
        ~String()
        {
            if(--_count==0)
            {
                delete []_pData;
            }
        }
        String(String &str)
        :_pData(str._pData)
        {
            str._count++;
            _count=str._count;
        }
    private:
        char *_pData;
        int _count;
};

string s1="hello world";
string s2(s1);

//s1构造,s2拷贝构造:s1和s2指向同一空间,s1和s2的_count都变成2
//当s2先析构,s2的_count--变成1,不释放
//当s1析构时,s1的_count--变成1,不释放
//造成内存泄露

             static int _pCount;那么每个对象的实例化都拥有这唯一的一个引用计数,共享范围过大   

class String
{
    public:
        String(pData=NULL)
        :_pData(new char[strlen(pData)+1])
        {
            _count=1;
            strcpy(_pData,pData);
        }
        ~String()
        {
            if(--_count==0)
            {
                delete []_pData;
            }
        }
        String(String &str) //不加const,不然底下的浅拷贝会出错
        :_pData(str._pData)
        {
            str._count++;
        }
    private:
        char *_pData;
        static int _count;  //静态的成员变量要在类外进行初始化
};
int String::_count=0;

string s1="hello world";
string s2(s1);
string s3("error");

//s1构造,s2拷贝构造:s1和s2指向同一空间,_count都变成2
//s3构造,_count变成1
//当s3先析构,_count--变成0,释放
//s1,s2造成内存泄露

int *_pCount;可以实现引用计数。

class String
{
    public:
        String(pData=NULL)
        :_pData(new char[strlen(pData)+1])
        ,_pCount(new int(1))
        {
            strcpy(_pData,pData);
        }
        ~String()
        {
            if(--(*_pCount)==0)
            {
                delete []_pData;
                delete _pCount;
            }
        }
        String& operator=(const String *str)
        {
            if(_pData!=str._pData)
            {
                if(--(*_pCount)==0)
                {
                    delete _pCount;
                    delete []_pData;
                }
                (*str._pCount)++;
                _pCount=str._pCount;
                _pData=str._pData;
            }
            return *this;
        }
        String(String &str) //不加const,不然底下的浅拷贝会出错
        :_pData(str._pData)
        ,_pCount(str._pCount)
        {
            (*str._pCount)++;
        }
    private:
        char *_pData;
        int *_pCount;  
};

这些字符串都是在堆上开辟的,那么引用计数也可以在堆上开辟,要从逻辑上,看引用计数是个指针,存次数,从物理上看,引用计数应该和字符指针放在一起,便于管理。让数据相同的对象都可以共享同一片内存。

综上,引用计数的设计如图:通过STL中的string看写时拷贝和读时拷贝_第1张图片

(3)引用计数什么时候需要共享呢?

          情况1:string s2(s1);     //s2拷贝自s1,即s2中的数据和s1的一样

          情况2:string s2; s2=s1;//s2的数据由s1赋值而来,即s2中的数据和s1的一样

     综上所述:

            string类中的拷贝构造和赋值运算符重载需要引用计数


 4)什么情况下需要进行写时拷贝

           对内容有修改时


 (5)c++版实现代码

class String
{    
    private:
        char *_pData;  //引用计数存在于_pData[-1]
    public:
        //构造函数
        String(pData=NULL)
        :_pData(new char[strlen(pData)+1+sizeof(int)])
        {
              //强转在头上4个字节存放引用计数的值
               (*(int *)_pData)=1;
              //恢复其字符串的长度
              _pData+=4; 
              strcpy(_pData,pData);
        }
        //拷贝构造
        String(const String&str)
        :_pData(str. _pData)
        {
           
           //(*(--(int *)str._pData)) ++;  
           //这个版本是错的,大家看看错在哪里?可以留言告诉我哦
            (*(--(int*)_pData-1)++;
        }
        //赋值运算符重载
        String& operator=(const String &str)
        {
            if(_pData!=str._pData)
            {
                if(--(*(--(int*)_pData-1)==0)
                {
                    _pData-=4;
                    delete []_pData;
                }
                
                else
                {
                    _pData=str._pData;
                    (*(--(int*)_pData-1)++;
                }
            }
            return *this;
        }
        //析构函数
        ~String()
        {
            if(--(*(--(int*)_pData-1)==0)
                {
                    _pData-=4;
                    delete []_pData;
                }  
        }  
};

2、读时拷贝(copy-on-read)

当C++的STL库中的string被这么利用时:

string s1="hello world";
long begin =  getcurrenttick();
for(size_t i=0;i