C++ 簡易string類實現(二)-引用計數

引用計數(reference count),允許多個等值對象共享一個實值,此技術的發展有兩個動機.第一,簡化heap object的簿記工作,在程序執行過程中,對象的擁有權可以會轉移,記錄對象的擁有權不是一件簡單的事情,其次,在對象沒有使用者時,需要自動銷燬自己,避免內存泄露;第二,如果多個對象擁有相同的值,那麼將那個值存儲多次往往是件愚蠢的事情,最好的做法是,讓所有等值對象共享一份實值就好,這麼做不僅節省內存,也使得程序速度加快,因爲不再需要構造和析構同值的多餘副本。

C++ 簡易string類實現(一)中我們看到,每一個String變量,都擁有一份heap object,即使其內部完全一樣,在某些情況下(僅僅對String變量讀操作,而沒有寫),就會引發上述中的第二個問題,因此,這裏通過引用計數的方式,來解決這個問題.在這裏,爲了節約篇幅,僅僅寫出主要的幾個成員函數.

類聲明:

class String
{
public:
    String(const char* str_ = "");

    String(const String& str_);

    String& operator=(const String& str_);

    ~String();

public:
    const char& operator[](size_t index_) const;

public:
    size_t getRefCount() const;

private:
    struct StringValue
    {
        size_t refCount;
        char* ptr;

        StringValue(const char* str_);

        ~StringValue();
    };

    StringValue* _value;
};

引用計數,顧名思義,是用來計數的,這裏,就需要明確是對String本身計數還是對其擁有的資源進行計數,可想而知,計數的應該是後者.在類String中,字符串的值和引用次數之間有耦合(coupling)關係,利用封裝的思想,將兩者封裝成一個類(class),不但存儲引用次數,也存儲他們所追蹤的對象值,這個class命名爲StringValue,並將其作爲String的一個嵌套(nested)類.爲了讓所有的String成員函數(member function)都能夠訪問StringValue,將StringValue聲明爲struct,這與封裝的思想似乎背道而馳,但是,將StringValue用private修飾,使僅能被String的成員函數訪問,而不被任何其它人訪問,這是在訪問的便利性和封裝之間做了一個折衷,畢竟,作爲一個輔助類,其作用範圍有限(String內),不希望追求極致的封裝而將StringValue本身加入過多額外的成員函數.

類實現:

String::String(const char* str_ /* = "" */)
    : _value(new StringValue(str_))
{
}

String::String(const String& str_)
    : _value(str_._value)
{
    ++_value->refCount;
}

String& String::operator=(const String& str_)
{
    /*
    //這樣寫存在問題,例如str2 = str3; str2 = str3;即重複一次,
    //如果是這種寫法,那麼會執行後續代碼一次,雖然結果是對的
    //但造成了不必要的運行消耗
    if (this != &str_)
    {
        return *this;
    }*/
    if (_value == str_._value)
    {
        return *this;
    }

    std::cout << "operator=" << std::endl;

    if (--_value->refCount == 0)
    {
        delete _value;
    }

    _value = str_._value;
    ++_value->refCount;

    return *this;
}

String::~String()
{
    if (--_value->refCount == 0)
    {
        delete _value;
    }
}

const char& String::operator[](size_t index_) const
{
    //爲了簡化代碼,不引入_size變量記錄字符串長度
    if (index_ >= strlen(_value->ptr))
    {
        throw std::out_of_range("String out of range!");
    }
    return _value->ptr[index_];
}

size_t String::getRefCount() const
{
    return _value->refCount;
}

String::StringValue::StringValue(const char* str_)
    : refCount(1)
{
    ptr = new char[strlen(str_) + 1];
    strcpy(ptr, str_);
}

String::StringValue::~StringValue()
{
    if (ptr != nullptr)
    {
        delete[] ptr;
    }
}

問題1:

在這裏,僅僅重載了運算符[]的const版本,因爲這份代碼對讀操作是正常的,但涉及寫時就會出現和預期不一致的問題,因爲多個String共享一個字符串,如果其中一個String變量修改字符串,其結果是,所有String的字符串都被修改了.例如,以下代碼(沒有提供運算符non-const重載,實際上無法通過編譯,僅用於解釋上述):

String str1 = "123";
String str2 = str1;
str2[1] = 2;//沒有提供運算符non-const重載,該行實際上無法通過編譯

對str2的修改,str1內容也會發生變化.

String str1 = "123";
std::cout << str1[2];   //讀操作
str1[1] = 2;            //寫操作

因爲C++編譯期無法告訴我們operator[]是被用於讀取或寫,出於安全,這裏假設對operator[]的調用都是寫操作,以確保上述代碼中的問題不會出現,non-const的operator[]代碼如下:

char& operator[](size_t index_);

char& String::operator[](size_t index_)
{
    if (index_ >= _value->refCount)
    {
        throw std::out_of_range("String out of range!");
    }

    //本對象和其他String對象共享同一個實值
    if (_value->refCount > 1)
    {
        _value->refCount--;

        _value = new StringValue(_value->ptr);
    }

    return _value->ptr[index_];
}

上述對non-const的實現思路:和其它對象共享一份實值,直到我們必須對自己所擁有的那一份實值進行寫動作.這個觀念在計算機科學領域中有很長的歷史,特別是在操作系統領域,各進程(process)之間往往允許共享某些內存分頁(memory pages),直到它們打算修改屬於自己的那一分頁.這項技術是如此普及,因而有一個專用名稱:copy-on-write(寫時才複製).這是提升效率的一般化做法(也就是lazy evaluation,緩式評估)中的一劑特效藥.

問題2:

String s1 = "123456";
char* p = &s1[1];
String s2 = s1;
*p = '8';//希望修改s1[1],結果是s1,s2均修改了

如上述代碼,non-const的operator[]在上述代碼前,就會出現問題.

解決該問題的一個思路:爲每一個StringValue對象加上一個標誌(flag)變量,用於指示可否被共享.一開始,我們先豎立此標誌(表示對象可被共享),但只要non-const operator[]作用域對象值身上就將標誌清除.一旦標誌被清除(設爲false),可能永遠不再改變狀態.

需修改的代碼如下:

struct StringValue
{
    size_t refCount;
    bool shareable; //新增此行
    char* ptr;

    StringValue(const char* str_);

    ~StringValue();
};

String::StringValue::StringValue(const char* str_)
    : refCount(1),
    shareable(true) //新增此行
{
    ptr = new char[strlen(str_) + 1];
    strcpy(ptr, str_);
}

String::String(const String& str_)
{
    if (str_._value->shareable) //加入判斷條件
    {
        _value = str_._value;
        ++_value->refCount;
    }
    else
    {
        _value = new StringValue(str_._value->ptr);
    }
}

char& String::operator[](size_t index_)
{
    ...
    _value->shareable = false;      //新增此行
    return _value->ptr[index_];
}

上述兩個問題的解決,都是以數據安全爲前提,由此無法完全做到寫時才複製(copy-on-write),其根本原因是,C++編譯期無法告訴我們operator[]被用於讀取或寫(通過代理類(proxy class(代理類))可以解決這個問題).

問題3
看下述代碼:

void printRefCount(const String& s)
{
    std::cout << s.getRefCount() << std::endl;
}


int main(){
        {
            String s1 = "123456";
            printRefCount(s1);
            String s2 = s1;
            char* p = &s2[1];
            printRefCount(s1);
            printRefCount(s2);
            String s3;
            s3 = s2;
            printRefCount(s3);
        }

    system("pause");
    return 0;
}

輸出:
這裏寫圖片描述
由於代碼:

char* p = &s2[1];

導致s2不可以被共享,因此複製運算符函數爲:

String& String::operator=(const String& str_)
{
    /*
    //這樣寫存在問題,例如str2 = str3; str2 = str3;即重複一次,
    //如果是這種寫法,那麼會執行後續代碼一次,雖然結果是對的
    //但造成了不必要的運行消耗
    if (this != &str_)
    {
        return *this;
    }*/
    if (_value == str_._value)
    {
        return *this;
    }

    std::cout << "operator=" << std::endl;

    if (--_value->refCount == 0)
    {
        delete _value;
    }

    if (str_.isShareable())
    {
        _value = str_._value;
        ++_value->refCount;
    }
    else
    {
        _value = new StringValue(str_._value.ptr);
    }

    return *this;
}

全部代碼:

class String
{
public:
    String(const char* str_ = "");

    String(const String& str_);

    String& operator=(const String& str_);

    ~String();

public:
    const char& operator[](size_t index_) const;

    char& operator[](size_t index_);

public:
    size_t getRefCount() const;

private:
    struct StringValue
    {
        size_t refCount;
        bool shareable;
        char* ptr;

        StringValue(const char* str_);

        ~StringValue();
    };

    StringValue* _value;
};
String::String(const char* str_ /* = "" */)
    : _value(new StringValue(str_))
{
}

String::String(const String& str_)
{
    if (_value->shareable)
    {
        _value = str_._value;
        ++_value->refCount;
    }
    else
    {
        _value = new StringValue(str_._value->ptr);
    }
}

String& String::operator=(const String& str_)
{
    /*
    //這樣寫存在問題,例如str2 = str3; str2 = str3;即重複一次,
    //如果是這種寫法,那麼會執行後續代碼一次,雖然結果是對的
    //但造成了不必要的運行消耗
    if (this != &str_)
    {
        return *this;
    }*/
    if (_value == str_._value)
    {
        return *this;
    }

    std::cout << "operator=" << std::endl;

    if (--_value->refCount == 0)
    {
        delete _value;
    }

    if (str_.isShareable())
    {
        _value = str_._value;
        ++_value->refCount;
    }
    else
    {
        _value = new StringValue(str_._value.ptr);
    }

    return *this;
}

String::~String()
{
    if (--_value->refCount == 0)
    {
        delete _value;
    }
}

const char& String::operator[](size_t index_) const
{
    //爲了簡化代碼,不引入_size變量記錄字符串長度
    if (index_ >= strlen(_value->ptr))
    {
        throw std::out_of_range("String out of range!");
    }
    return _value->ptr[index_];
}

char& String::operator[](size_t index_)
{
    if (index_ >= strlen(_value->ptr))
    {
        throw std::out_of_range("String out of range!");
    }

    //本對象和其他String對象共享同一個實值
    if (_value->refCount > 1)
    {
        _value->refCount--;

        _value = new StringValue(_value->ptr);
    }

    _value->shareable = false;
    return _value->ptr[index_];
}

size_t String::getRefCount() const
{
    return _value->refCount;
}

String::StringValue::StringValue(const char* str_)
    : refCount(1),
     shareable(true)
{
    ptr = new char[strlen(str_) + 1];
    strcpy(ptr, str_);
}

String::StringValue::~StringValue()
{
    if (ptr != nullptr)
    {
        delete[] ptr;
    }
}

注:以上內容是閱讀< < more effective C++> > item 30 的總結;

發佈了23 篇原創文章 · 獲贊 10 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章