C++基础之string写时复制(代理模式)

前言

个人学习笔记


       C++发展历史上实现string的方式有很多种,但基本遵从以下三种方式:
  • 1、Eager Copy(深拷贝):无论什么情况,都采用拷贝字符串内容的方式解决;这种实现方式,在需要对字符串进行频繁复制而又并不改变字符串内容时,效率比较低下。所以需要对其实现进行优化,之后便出现了COW的实现方式。
  • 2、COW(Copy-On-Write,写时复制):当两个std::string发生复制或者赋值时,不会复制字符串内容,而是增加一个引用计数,然后字符串指针进行浅拷贝,其执行效率为O(1)。只有当修改其中一个字符串内容时,才执行真正的复制。即:浅拷贝 + 引用计数。Ubuntu14.04创建的string对象就是写时复制,大小为8。当执行复制或赋值时,引用计数加1,std::string对象共享字符串内容;当std::string对象销毁时,并不直接释放字符串所在的空间,而是先将引用计数减1,直到引用计数为0时,才真正释放字符串内容所在的空间。
  • 3、SSO(Short String Optimization,短字符串优化):一个程序里用到的字符串大部分都很短小,而在64位机器上,一个char*型指针就占用了8个字节,所以SSO就出现了,其核心思想是发生拷贝时要复制一个指针,对小字符串来说,为啥不直接复制整个字符串呢,说不定还没有复制一个指针的代价大。所以SSO执行的策略就是当字符串的长度小于等于15个字节时,buffer直接存放整个字符串,就是把字符串存放到栈上;当字符串大于15个字节时,buffer存放的就是一个指针,指向堆空间的区域,就是把字符串存放在堆上。这样做的好处是:当字符串较小时,直接拷贝字符串,放在string内部,不用获取堆空间,开销小。Ubuntu18.04创建string对象就是SSO,大小为32。

COW的实现(代理模式)

       写时复制的目的就是,对于n个内容相同的字符串来说,如果都进行深拷贝的话势必会浪费很多存储空间,所有对于不需要写操作的字符串来说,就只进行浅拷贝,让这些变量都指向这里,当需要更改的时候,再进行深拷贝,对深拷贝的字符串进行写操作,这就是写时复制,可以节省很多内存空间,所以实现自定义的String的时候一定要区分读写操作,如果直接对自定义String类的下标运算符重载来进行写操作的话,那么通过下标运算符读某个字符串的时候也会写操作,因为都要经过同样的操作,这样就无法区分读写操作了,所以行不通;那么能否通过赋值运算符和输出流运算符的重载来区分读写呢?重载赋值运算符进行写操作,重载输出流运算符进行读操作,我们知道运算符的重载需要至少一个自定义类型或者枚举类型,而赋值运算符(=)的两边都是char类型,所以不能重载赋值运算符,所以此时无法通过赋值运算符和输出流运算符的重载来区分写操作;又因为赋值运算符的左边与输出流运算符的右边都是通过下标访问运算符的重载之后获得的返回值,刚刚已经说过不能通过下标访问运算符的重载来区分读写操作,所以从目前的分析来看无法实现写时复制了。其实本来可以通过赋值运算符和输出流运算符的重载来区分读写操作的,只是因为赋值运算符无法重载,而导致失败,那么如果赋值运算符可以重载呢,是不是就意味着这个问题可以解决,如果赋值运算符无法重载等式2边都是内置类型的,那么如果通过下标运算符重载返回的不是一个char类型的是不是就可以重载了,只要赋值运算符可以重载,那么写时复制也就不难实现了!这时我们的思路就是通过重载下标访问运算符返回一个自定义类型,然后对该自定义类型的赋值运算符和输出流运算符进行重载,这个问题不就顺理成章的解决了!所以这里我们需要借助一个代理商(CharProxy),重载String的下标访问运算符,返回一个代理商类型,然后重载该代理商的赋值运算符和输出流运算符,这样就可以区分读写操作。因为正常情况下String类的下标访问运算符的重载返回的是一个char类型,所以String不可以重载赋值运算符,通过一个自定义的CharProxy类型,这样就可以重载赋值运算符,结合输出流运算符的重载就可以达到区分读写的目的,这个CharProxy就是帮String完成这些功能,这就是所谓的代理模式
       因为String要实现写实复制,所以在申请一块空间存储字符串的时候,除了要多申请一个字节存储’\0’以外,还要在该空间的最开始的位置划出4个字节来存储引用计数,所以默认构造函数和传参C风格字符串的构造函数对对象存储空间的申请都要加上额外的5个字节,并将指针向前偏移4个字节来操作字符串,对引用计数初始化需要往回偏移4个字节并强转成int型,才可以操作引用计数申请的4个字节的空间,此时一定要注意指针的位置;对于拷贝构造函数就只需要增加引用计数即可;赋值运算符的重载需要注意一点,需要被赋值的字符串的引用计数减1,赋值的字符串引用计数加1,在释放左操作数的时候把引用计数申请的4个字节空间一起回收掉;析构函数这里,对于指向同一字符串的对象,释放掉一个对象,引用计数减1,并不需要对每个对象都delete,只需要对引用计数为0的最后一个对象delete释放掉即可。由于CharProxy只需要为String服务,所以这里把CharProxy设为私有,再对CharProxy的赋值运算符和输出流运算符重载即可,赋值运算符的重载就是完成写时复制的操作,输出流运算符重载就是实现读的操作。最后,因为我们需要String下标访问运算符最终返回的就是一个char类型的字符,这样可以不用重载输出流运算符,所以这里我们通过类型转换函数,将自定义的CharProxy转换为char,代替输出流运算符的重载,简化操作。

#include 
#include 

using std::cout;
using std::endl;
class String
{
public:
    String()
    :_pstr(new char[5]() + 4)
    {
        cout << "String()" << endl;
        //_pstr创建的是一个char型指针,需要转换成int型指针控制4个字节
        *(int *)(_pstr - 4) = 1;
    }
    //String s1 = "hello";
    String(const char *pstr)
    :_pstr(new char[strlen(pstr) + 5]() + 4)//new char[strlen(pstr) + 5](),加了()后,数据都初始化为0了,就相当于memset,已经初始化了并设置为0了。
    {
        cout << "String(const char *)" << endl;
        strcpy(_pstr, pstr);
        *(int *)(_pstr - 4) = 1;
    }
    //String s2 = s1;
    String(const String &rhs)
    :_pstr(rhs._pstr)
    {
        cout << "String(const String &)" << endl;
        ++*(int *)(rhs._pstr - 4);
    }
    int getRef()
    {
        return *(int *)(_pstr - 4);
    }
    size_t size() const
    {
        return strlen(_pstr);
    }
    //String s3 = "world"
    //s3 = s1;
    String &operator=(const String &rhs)
    {
        cout << "String &operator=(const String &)" << endl;
        if(this != &rhs)//1、自复制
        {
            /* auto ref = --*(_pstr - 4); */
            --*(int *)(_pstr - 4);
            if(0 == *(int *)(_pstr - 4))//2、释放左操作数
            {
                /* delete []_pstr; //error*/
                //这里需要先把指针偏移,然后delete
                delete [](_pstr - 4);
                _pstr = nullptr;
            }
            _pstr = rhs._pstr;//3、浅拷贝
            ++*(_pstr - 4);
        }
        return *this;//4、返回this指针
    }
    //将String风格字符转换成C风格
    const char* c_str() const
    {
        return _pstr;
    }
    ~String()
    {
        cout << "~String()" << endl;
        //析构函数这里,对于指向同一字符串的对象,释放掉一个对象,引用计数减一
        //并不需要对每个对象都delete,只需要对引用计数为0的
        //最后一个对象delete释放掉即可,
        --*(int *)(_pstr - 4);
        if(0 == *(int *)(_pstr - 4))
        {
            //这里需要先把指针偏移,然后delete
            delete [](_pstr - 4);
            /* delete []_pstr;//error */
        }
        /* if(nullptr == _pstr) */
        /* { */
        /*     delete [] _pstr; */
        /*     _pstr = nullptr; */
        /* } */
    }
    friend std::ostream &operator<<(std::ostream &os, const String &rhs);
private:
    //设计模式之代理模式
    class CharProxy
    {
    public:
        CharProxy(String &str, size_t idx)
        :_str(str)
        ,_idx(idx)
        {
            cout << "CharProxy(String &, size_t)" << endl;
        }

        char &operator=(const char &ch)
        {
            if(_idx < _str.size())
            {
                if(_str.getRef() > 1)
                {
                    --*(int *)(_str._pstr - 4);
                    char *tmp = new char[strlen(_str._pstr)]() + 4;
                    strcpy(tmp, _str._pstr);
                    _str._pstr = tmp;
                    cout << "tmp.sizeof = " << strlen(tmp) << endl;
                    *(int *)(_str._pstr - 4) = 1;
                }
                _str._pstr[_idx] = ch;//真正的的赋值操作
                return _str._pstr[_idx];
            }
            else
            {
                static char nullchar = '\0';
                return nullchar;
            }
        }
        //类型转换函数,将自定义的CharProxy转换为char
        //代替输出流运算符的重载
        operator char()
        {
            return _str._pstr[_idx];
        }
        /* friend std::ostream &operator<<(std::ostream &os, const CharProxy &rhs); */
    private:
        String &_str;
        size_t _idx;
    };
public:
    //CharProxy要在String下标运算符重载之前定义
    //不可以返回CharProxy类型的引用,因为return的是临时变量是个右值
    CharProxy operator[](size_t idx)
    {
        return CharProxy(*this, idx);
    }
    /* friend std::ostream &operator<<(std::ostream &os, const CharProxy &rhs); */
private:
    char *_pstr;
};
std::ostream &operator<<(std::ostream &os, const String &rhs)
{
    //判断一下rhs的数据成员是否为空
    if(rhs._pstr)
    {
        os << rhs._pstr;
    }
    return os;
}
#if 0
//因为rhs要同时访问String和CharProxy的私有成员
//所以要将该输出流运算符同时设置为String和CharProxy的友元函数
std::ostream &operator<<(std::ostream &os, const String::CharProxy &rhs)
{
    os << rhs._str._pstr[rhs._idx];
    return os;
}
#endif
int main()
{
    String s1("hello");
    String s2 = s1;
    String s3("world");
    s3 = s1;
    cout << "s1.getRef = " << s1.getRef() << endl;
    cout << "s2.getRef = " << s2.getRef() << endl;
    cout << "s3.getRef = " << s3.getRef() << endl;
    cout <<"s1 = " << s1 << endl << "s2 = " << s2 << endl << "s3 = " << s3 << endl;
    printf("s1's address = %p\n", s1.c_str());
    printf("s2's address = %p\n", s2.c_str());
    printf("s3's address = %p\n", s3.c_str());
    
    cout << endl;
    s3[0] = 'H';
    cout << "s1.getRef = " << s1.getRef() << endl;
    cout << "s2.getRef = " << s2.getRef() << endl;
    cout << "s3.getRef = " << s3.getRef() << endl;
    cout <<"s1 = " << s1 << endl << "s2 = " << s2 << endl << "s3 = " << s3 << endl;
    printf("s1's address = %p\n", s1.c_str());
    printf("s2's address = %p\n", s2.c_str());
    printf("s3's address = %p\n", s3.c_str());

    cout << endl;
    //s[0]将自动匹配可以转换的类型,CharProxy类中定义了char的类型转换函数
    //所以此时s[0]将自动从CharProxy类型转换成char,所以可以直接输出,而不需要重载CharProxy的输出流运算符
    cout << "s3[0]" << s1[0] << endl;
    cout << "s1.getRef = " << s1.getRef() << endl;
    cout << "s2.getRef = " << s2.getRef() << endl;
    cout << "s3.getRef = " << s3.getRef() << endl;
    cout <<"s1 = " << s1 << endl << "s2 = " << s2 << endl << "s3 = " << s3 << endl;
    printf("s1's address = %p\n", s1.c_str());
    printf("s2's address = %p\n", s2.c_str());
    printf("s3's address = %p\n", s3.c_str());
    return 0;
}

你可能感兴趣的:(C++学习笔记,c++,字符串,指针,编程语言)