C++ String的引用计数写时复制 的实现 《More Effective C++》

Posted 小丑快学习

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ String的引用计数写时复制 的实现 《More Effective C++》相关的知识,希望对你有一定的参考价值。

1.引用计数

c++引用计数的可以节省内存,而且同时可以降低构建对象和析构的开销,所谓引用计数简单说来就是对各对象共享一份实体的数据,但是我们需要实现对该数据的引用的对象的记录,这样最后一个对象引用结束后能够安全的删除数据。
用字符串举例,假设我们想要实现字符串的拷贝或者赋值,那么我们想要呈现的客户的是各自独立的字符串。如下:
在这里插入图片描述
但是,对于计算机的内部实现而言,这样的方式显然出现了冗余存储的现象,那么我们期待计算机内部是这样实现的。
在这里插入图片描述
这样所有的用户拥有的字符串都是同一个,但是,这会出现一个问题,那就是的那个其中一个销毁对象时,其它对象的数据也将不可访问,因此,我们需要对该份数据的引用进行计数,只有最后一个引用对象才能真正的销毁数据。
因此实现是这样的。
在这里插入图片描述

2.写时复制

上述实现计数方式虽然就实现了引用计数,能够使得多个相同的字符串共享同一份数据,但是,当其中任何一个对象修改数据时,其它对象所拥有的数据也就都改变,因此,为了避免这种情况的发生,采用写时复制的技巧(copy on write),也就是当某个对象要修改数据时便重新赋值一份进行修改,从而该对象就有一份新的数据,不在和其它对象共享一份数据。

但是如果出现这样的语句时可能就不那么满意了。

String s1 = "Hello";
char *p = &s1[1];
String s2 = s1;
*p = 'x';

指针p并不能被计数器记录,因此,这将导致共享数据被修改。
在这里插入图片描述
为了防止上述情况的出现,当有字符串对共享数据有修改倾向时,则把该数据设为不可共享状态,那么上述的语句中,s2将不会和s1共享数据,而是复制一份新的数据。

下面是实现代码,具有详细的注释:

//引用计数实现string
class My_string {
public:

    My_string(const char* val = ""):value(new ref_count(val))                            
    {}
    My_string(const My_string& other)                          
    {
    	//数据可以共享则直接引用,否则重新分配
        if (other.value->isShareable) {
            
            value = other.value;
            ++(value->count);
        }
        else
        {
            value = new ref_count(other.value->data);
        }
        
    }
    
    My_string & operator=(const My_string & other) {
        //检查自我赋值
        if (this->value == other.value) {
            return *this;
        }
        //如果是最后一个引用,则删除数据
        if (-- value->count == 0) {
            delete this->value;
        }
        //如果不可以共享,则需要重新分配空间
        if (other.value->isShareable) {
            value = other.value;
            ++value->count;
        }
        else {
            value = new ref_count(other.value->data);
        }
        return *this;
    }

    ~My_string() {
        if ( -- value->count == 0 ) {
            delete value;
        }
    }

    const char& operator[]( int val)  const 
    {
        return value->data[val];
    }

    //可能会对数据进行修改,所以采用写时复制的策略
    char & operator[](int val) {

        if (value->count == 1) {//如果目前只有一个引用则不用复制
            return value->data[val];
        }
        --value->count;
        value = new ref_count(value->data);
        value->isShareable = false;//不能共享,以防 char * p = &s[i] 这种方式出现
        return value->data[val];
    
    }

private:
    //设置为友元函数,以便能访问私有数据
    friend ostream& operator<< ( ostream & cout, const My_string& s);
   
    //对数据的封装
    struct ref_count {
        //字符串数据
        char* data;
        //是否可共享标志,仅在可能写时设置为false
        bool isShareable;
        //计数器
        int count;
        //构造函数
        ref_count(const char * data_):count(1),isShareable(true){
            data = new char[strlen(data_) + 1];
            strcpy_s(data , strlen(data_) + 1 , data_);
        }
        //析构函数
        ~ref_count() {
                delete[] data;
        }
    };
    //每个字符串拥有一个指向封装数据的指针
    ref_count * value;
};

// << 运算符重载
ostream& operator<< (ostream& cout ,const My_string & s) {
    for (char* p = s.value->data; *p != '\\0'; p++) {
        cout << *p;
    }
    return cout;
}

3.代理类区别读写操作

上述的字符串对于随机访问操作符[ ]而言,不管是都读或者写都将会把数据设置为不可共享的状态,因而这将导致数据的共享性并不高,因为对于字符串的操作,随即操作使用的很多,因而,我们需要用某种方式使得读和写能有所区别。使用一个内嵌的代理类能够解决这样的问题。如下代码:

class My_string {
public:

    //构造函数
    My_string(const char* val = "") :value(new ref_count(val))
    {}

    My_string(const My_string& other)
    {
        value = other.value;
        ++value->count;
    }

    //代理类,用于实现对读写的判断,或者左值和右值的引用
    class CharProxy{
    public:
        CharProxy( My_string & s_, int index_ ):s(s_),index(index_)
        {
        }

        CharProxy & operator=(const CharProxy & cp) {

            set_String_value();
            s.value->data[index] = cp.s.value->data[index];
            return *this;
        }

        CharProxy& operator=(char c) {
            set_String_value();
            s.value->data[index] = c;
            return *this;
        }

        //装换为char的隐式类型转换
        operator char() {
            return s.value->data[index];
        }
        
    private:
        //重复代码提出,减少代码重复
        void set_String_value() {
            if (s.value->isShared()) {
                --s.value->count;//先将原来的计数减一,在复制
                s.value = new ref_count(s.value->data); 
            }
        }

        My_string& s;
        int index;
    };

    //返回值应该是代理类
    const CharProxy operator[](int index) const
    {
        return CharProxy(const_cast<My_string&>(*this), index);
    }

    CharProxy operator[](int index) {
        return CharProxy(*this, index);
    }
    
    My_string & operator=(const My_string & other) {
        //检查自我赋值
        if (this->value == other.value) {
            return *this;
        }
        //如果是最后一个引用,则删除数据
        if (-- value->count == 0) {
            delete this->value;
        }
        value = other.value;
        ++value->count;
        return *this;
    }

    ~My_string() {
        if ( -- value->count == 0 ) {
            delete value;
        }
    }

   

private:
    //设置为友元函数,以便能访问私有数据
    friend ostream& operator<< ( ostream & cout, const My_string& s);
    friend class CharProxy;//代理类需要访问String的成员value

    //对数据的封装
    struct ref_count {
        //字符串数据
        char* data;
        //是否可共享标志,仅在可能写时设置为false
        bool isShareable;
        //计数器
        int count;
        //构造函数
        ref_count(const char * data_):count(1),isShareable(true){
            data = new char[strlen(data_) + 1];
            strcpy_s(data , strlen(data_) + 1 , data_);
        }
        //析构函数
        ~ref_count() {
                delete[] data;
        }

        //设为不可共享
        void makeUnshareable() {
            isShareable = false;
        }

        //查看是否被共享
        bool isShared() {
            return count > 1;
        }

    };
    //每个字符串拥有一个指向封装数据的指针
    ref_count * value;
};

// << 运算符重载
ostream& operator<< (ostream& cout ,const My_string & s) {
    for (char* p = s.value->data; *p != '\\0'; p++) {
        cout << *p;
    }
    return cout;
}

使用CharProxy类来代表char,这样对于My_string的操作符[ ]的返回值将会是一个CharProxy对象,而该对象中有对该字符串的引用,如果执行如下的操作:

My_string s("hello world!");
s[1] = 'c';

则对于s[1]将会返回一个CharProxy的临时对象,而赋值运算符将会调用该临时对象的重载版本,即如下的版本:

 CharProxy& operator=(char c) {
     set_String_value();
     s.value->data[index] = c;
     return *this;
}

 //重复代码提出,减少代码重复
void set_String_value() {
     if (s.value->isShared()) {
         --s.value->count;//先将原来的计数减一,在复制
         s.value = new ref_count(s.value->data); 
     }
 }

只有调用赋值运算符才会去执行写时复制操作(set_String_value中函数实现),因而,如果客户端不进行赋值,则不会执行写时操作,因而,实现对读写的区分。
而另一个重载版本:

CharProxy & operator=(const CharProxy & cp) {

     set_String_value();
     s.value->data[index] = cp.s.value->data[index];
     return *this;
     }

这个版本的赋值运算时为了应对这种情况的赋值运算:

s[1] = s1[1];

通过代理类的实现,我们则不再需要设立标志来判定是否能够共享了,也不需要将数据设为不可共享状态,因而My_string的部分成员函数也应该做出相应的修改。

本文代码参考《More Effective C++》条款29、30写出。更多细节和请仔细阅读此书。

以上是关于C++ String的引用计数写时复制 的实现 《More Effective C++》的主要内容,如果未能解决你的问题,请参考以下文章

C++ 深浅拷贝写时拷贝

C++ String 写时拷贝

C++ 深浅拷贝写时拷贝

string类的写时拷贝与引用计数

string类的写时拷贝与引用计数

String 类的实现写时拷贝浅析