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

前言:上一篇文章实现了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
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值