string类的简单实现(写时拷贝Copy-on-write)

来源:互联网 发布:南昌广州seo外包 编辑:程序博客网 时间:2024/06/05 21:09

前言:上一篇文章实现了string的深拷贝写法;那我们能不能用浅拷贝写string类呢?当然可以;
一、
(1)
当我们需要对拷贝之后的对象进行修改时,采用深拷贝的方式; 如果不需要修改,只是输出字符串的内容时,或者是当偶尔修改的的时候,我们再采用深拷贝的方法;对于这两种情况,我们在实现拷贝构造函数时采用浅拷贝的方式。在采用浅拷贝的方式以后,那么两个对象的成员变量指针指向同一块字符串空间;根据上一篇的文章,我们知道这样的话,析构时必然会发生错误;那么怎么避免析构时出错;请看下文;

(2)
写之前,还是先说一下,为什么要实现string类的写时拷贝

原理:浅拷贝+引用计数;

因为在现实生活中,我们有很多情况下用一个对象来拷贝构造另一个对象的之后,我们只是把拷贝构造来的对象用一下而已,不会对他的字符串进行修改操作;那么这样的话,我们其实不用深拷贝那么麻烦。按照上一篇的深拷贝,如果我们要用一万个对象,那我们开辟一万个对象空间,但是得到之后只是用对象里面保存的字符串,并没有其他的操作;这样的话会很浪费空间;而且还要拷贝字符串,程序效率也会降低,所以我们想到了一种实现方式;就是说

只有当我们要对拷贝来的字符串进行修改时,才采用深拷贝的方式,其余情况都采用浅拷贝;这种只有写的时候才进行深拷贝的方式叫做写时拷贝;

(3)实现写时拷贝的时候,因为采用了浅拷贝的方式,所以,为了避免析构的时候出现错误,上篇文章的析构函数就用不了;要在其基础上修改;我们可以在成员变量中添加一个变量count,用来记录拷贝的次数;每拷贝一次,我们就count++;然后在析构函数内部也加上if的判断语句;当我们每次析构一个对象的时候,就–count;当count等于0的时候,再进入if语句内部;释放空间;将变量置空;

再此,我给出了三种方案,其中有二个是错的;但都是经典的错误写法。所以把三种方法都说一下

二、两种经典的错误实现方法:

(1)方案一:错误写法

方案一:(错误)#include<iostream>using namespace std;class String{public:    String(const char* str="")//构造函数        :_str(new char[strlen(str)+1])        ,_count(1)    {        strcpy(_str,str);    }    String( String& s)//拷贝构造---浅拷贝        :_str(s._str)    {        _count=++(s._count);    }    String& operator=(String s)//赋值操作符重载---现代写法    {        std::swap(_str,s._str);        return *this;    }    ~String()//析构函数    {        if (--_count==0)        {            delete[] _str;            _str=NULL;        }    }private:    char* _str;    int   _count;};int main(){    String s1("abcdef");    String s2(s1);    return 0;}

分析:
分析:首先这种方案是不对的,为什么呢?我们确实是添加了一个计数器_count;在构造函数里面;每次构造一个对象;_count赋值为1;在析构函数内部也加入了if(–_count=0)的判断机制,避免一块空间被多次释放;但是真的达到这个效果了吗?其实没有,因为在拷贝构造函数当中,我们每次++被拷贝的_count然后赋给要接受拷贝的对象的_count;当程序走完,此时两个_count都变为2;
这里写图片描述
当析构各自的对象时,这时,这两个对象的_conut各自都为2;当调用析构函数时,–_count以后,都不能够释放空间;这里的意思就是,当S2调用析构函数后,他没有释放空间,自己的_count变为了1;
这里写图片描述
但是接下来,当s1掉用析构函数时,它的_count不是1,仍然是2,–_count以后仍然不能够进入if语句释放空间;
这里写图片描述
所以,这种方法不对;那我们能不能定义一个_count使其能够被所有对象共用;当一个对象调用析构函数时,–_count;等到下一次另一个对象调用时,在上一次减了基础上再减呢;当然可以;我们把_count定义为局部静态变量;

(2)方案二:错误写法

在类中数据成员的声明前加上static,该成员是类的静态数据成员,必须在类外进行定义;

★★类中静态数据成员和非静态数据成员的区别:★★

①对于非静态数据成员,每个类对象都有自己的拷贝.

②而静态数据成员被当做是类的成员,无论这个类被定义了多少个,静态数据成员都只有一份拷贝, 为该类型的所有对象所共享(包括其派生类).所以,静态数据成员的值对每个对象都是一样的,它的值可以更新. 静态数据成员不属于某个类对象所私有,所以不能再在初始化列表中初始化

#include<iostream>using namespace std;#include<cassert>class String{public:    String(const char* str="")//构造函数        :_str(new char[strlen(str)+1])    {        _count=1;        strcpy(_str,str);    }    String(const String& s)//拷贝构造---浅拷贝        :_str(s._str)    {        ++_count;    }    String& operator=(String s)//赋值操作符重载---现代写法    {        std::swap(_str,s._str);        return *this;    }    ~String()//析构函数    {        if (--_count==0)        {            delete[] _str;            _str=NULL;        }    }private:    char* _str;    static int  _count;};int String:: _count=1;int main(){    String s1("abcdef");    String s2(s1);    String s3("HELLO");    return 0;}

分析: 这种写法表面上看上去好像是对的;其实也存在问题;如过只看我上面的两行测试代码;好像没有错误;当s1创建时,静态成员变量_count变为了1;然后构造S2之后,_count++;变为2;这时这个_count是两个对象的共用成员变量,

这里写图片描述

然后s2先调用析构函数,–_count变为1,

这里写图片描述

然后s1再调用类的析构函数,此时的_count为1,–_count以后,_count 变为0;这时,才将s1和s2共同指向的空间释放掉,避免一块空间,被多次释放;

这里写图片描述

but,当我们在这后面,再创建一个对象s3时,这就不对了,创建s3的时候,调用构造函数,将静态变量_count变为1;

创建s1
创建s2

此时类的共用成员变量_count变为了1;当从后往前析构时;

创建s3

S3调用析构函数_conut变为0;s3的空间成功释放;

s3调用析构函数

但是当s2调用析构函数时,此时计数器_count=0;–count之后变为-1,不会进入if判断语句内部释放空间;

s2调用析构函数

S1调用析构函数时,_count变为-2;更不会析构s1和S2共同指向的空间。所以错误

s1调用析构函数

三、正确的写时拷贝写法

(1)方案三:正确写法

#include<iostream>using namespace std;#include<cstring>class String{public:    String(const char* str="")//构造        :_str(new char[strlen(str)+1])        ,_PCount(new int(1))    {        strcpy(_str,str);    }    String(const String& s)//拷贝构造----浅拷贝        :_str(s._str)        ,_PCount(s._PCount)    {        ++_PCount[0];    }    String& operator=(const String& s)//赋值运算符重载    {        if (this!=&s)        {            delete[] _str;            delete _PCount;            _str=s._str;            _PCount=s._PCount;            ++_PCount[0];        }        return *this;    }    ~String()//析构    {        if (--_PCount[0]==0)        {            delete[] _str;            delete _PCount;            _str=NULL;            _PCount=NULL;        }    }private:    char* _str;    int* _PCount;};void Test(){    String s1("abcdef");    String s2(s1);    String s3("ABCEDF");    String s4;    s4=s1;}int main(){    Test();    return 0;}

分析:
正确写法(每段字符串内存加上属于自己的计数器指针,这样的话,只有当拷贝构造的时候,两个对象的计数器指针才会指向同一个计数器内存,让其++,如果构建新的对象,那么他的计数器为构造函数1,析构时,是自己的1变为0,不会影响拷贝构造的两个对象的计数器变化,这样就能到达:没有拷贝构造的对象,一次把自己的空间释放掉,而拷贝构造的对象,所有对象的计数器都指向同一个计数器空间,释放该计数器的字符串内存;也不会出错)
s1,s2,s3都构造后,s3不会影响s1,s2的计数器变化
S3调用析构函数成功
s2调用析构没有释放,s1调用析构,空间释放,指针置空
图示说明:

    String s1("abcdef");    String s2(s1);    String s3("ABCEDF");

这里写图片描述

②赋值运算符重载详解

String& operator=(const String& s)//赋值运算符重载    {        if (this!=&s)        {            delete[] _str;            delete _PCount;            _str=s._str;            _PCount=s._PCount;            ++_PCount[0];        }        return *this;    }

测试

    String s1("abcdef");    String s2(s1);    String s3;    s3=s1;

这里写图片描述

(2)方案三写时拷贝的改进

前面的方案三,我们是用两个指针,分别管理内存和计数器,这样每次构造函数的时候,要分别开辟空间给各自的指针;这样做降低效率;我们可以只开辟一次空间;里面既存计数器又存字符串;在开辟的空间的前四个字节存储计数器,后面存储字符串;

#include<iostream>using namespace std;#include<cstring>#include<cassert>class String{public:    String(const char* str="")//构造        :_str(new char[strlen(str)+1+4])    {        cout<<"构造"<<endl;           _str+=4;//走到数据区首地址        strcpy(_str,str);        GetCount()=1;//计数器区域赋值为1    }    String(const String& s)//拷贝构造        :_str(s._str)    {        cout<<"拷贝构造"<<endl;           ++GetCount();//将所指向的共同内存的计数器加+    }    String& operator=(String& s)//赋值运算符重载---赋值的对象计数器要++;被赋值的对象计数器要++    {        if (this!=&s)        {            cout<<"构赋值运算符重载"<<endl;               /*判断被赋值的对象的计数器--,是否为0;如果为0,就要释放他的(计数区+数据区)              如果没有到达0;则只是给计数器减1;*/            if (--GetCount()==0)            {                delete[] (_str-4);//回到内存的首位置            }            ++(s.GetCount());            _str=s._str;        }        return *this;    }    ~String()//析构    {        cout<<"析构"<<endl;           if (--GetCount()==0)        {            cout<<"释放"<<endl;               delete[] (_str-4);            _str=NULL;        }    }    char& operator[](size_t index)//  写时拷贝    {        cout<<"重载[]"<<endl;        assert(index>=0 && index<(int)strlen(_str));        if (GetCount()=1)//如果该对象的计数器为1        {            return _str[index];//直接返回index位置的值,进行修改        }        如果不为1        --GetCount();//先给对象的计数器--        char* tmp=new char[strlen(_str)+1+4];//然后开辟一个同样大小的空间;用指针指着        strcpy(tmp+4,_str);//给这块空间字符串区域拷贝原空间的字符串        _str=tmp+4;//将指针指向新开辟字符串的首位置        GetCount()=1;//将指针的计算器置为1        return _str[index];//返回index位置的值    }private:    int& GetCount()//取计数器的值    {        return  *((int*)_str-1);    }private:    char* _str;};void Test(){   String s1("abcdef");   String s2(s1);   String s3(s2);   s3[3]='q';}int main(){    Test();    return 0;}

部分函数过程图示:
①拷贝构造函数
这里写图片描述

②赋值运算符重载
这里写图片描述

四、写时拷贝比普通拷贝的效率高

#include<iostream>using namespace std;#include<cstring>#include<cassert>#include<Windows.h>namespace WriteCopy{class String{public:    String(const char* str="")//构造        :_str(new char[strlen(str)+1+4])    {        _str+=4;        strcpy(_str,str);        GetCount()=1;    }    String(const String& s)        :_str(s._str)    {        ++GetCount();    }    ~String()//析构    {        if (--GetCount()==0)        {            delete[] (_str-4);            _str=NULL;        }    }private:    int& GetCount()//取计数器的值    {        return  *((int*)_str-1);    }private:    char* _str;};}namespace MyString{    class String    {       public:        String(const char* str="")            :_str(new char[strlen(str)+1])        {            strcpy(_str,str);        }        String(const String& s)            :_str(new char[strlen(s._str)+1])        {            strcpy(_str,s._str);        }        ~String()        {            if (NULL!=_str)            {                delete[] _str;                _str=NULL;            }        }    private:        char* _str;    };}void Test(){    int start=GetTickCount();    WriteCopy::String s1("abcdef");    for (int i=0;i<10000000;i++)    {        WriteCopy::String s2(s1);    }    int end=GetTickCount();    cout<<"写时拷贝"<<end-start<<endl;    start=GetTickCount();    MyString::String s3("abcdef");    for (int i=0;i<10000000;i++)    {        MyString::String s4(s3);    }    end=GetTickCount();    cout<<"普通拷贝"<<end-start<<endl;}int main(){    Test();    return 0;}

这里写图片描述
end!!!

接下来模拟实现c++库中的string类;

0 0
原创粉丝点击