1. 程式人生 > >使用引用計數和copy-on_write實現String類

使用引用計數和copy-on_write實現String類

cpp 不容易 The 簡單 但是 c++對象 之前 his 遷移

本文寫於2017-01-18,從老賬號遷移到本賬號,原文地址:https://www.cnblogs.com/huangweiyang/p/6295420.html

這算是我開始復習的內容吧,關於string類半年前寫過,最近拿出來溜溜,以免面試被問到結果自己忘了。我之前的博客地址:C++引用計數思想--利用引用計數器自定義String類。

首先上一個string類最簡明的寫法,沒有用到引用計數和COW,不過寫法實在是很簡單,不容易出錯。先看代碼,然後說弊端。

#include <iostream>
#include <string.h>

class my_string {
public:
    my_string(const char* str = NULL) {
        if(str == NULL){
            str_ = new char[1];
            *str_ = ‘\0‘;
        }   
        else{
            str_ = new char[strlen(str)+1];
            strcpy(str_, str);
        }   
    }   
    my_string(const my_string& other) 
        : str_(new char[other.size()]+1) {    //直接使用參數列表
            strcpy(str_, other.c_str());
    }   
    my_string& operator=(my_string other) {   //按值傳遞
        swap(other);
        return *this;
    }   
    ~my_string() {
        //delete []str_;
        str_ = NULL;
    }   
public:
    size_t size() const {
        return strlen(str_);
    }   
    const char* c_str() const {
        return str_;
    }
    void swap(my_string& other) {
        std::swap(str_, other.str_);
    }
public:
    void show() const {
        std::cout<<str_<<std::endl;
    }
private:
    char* str_;
};

上述就是最簡潔方案的代碼。不過,這種方案有一個弊端,甚至是錯誤。那就是我們不能在析構函數中直接delete []str_了。因為上述方案,可能造成兩個對象對字符串內存資源的共享。如果析構函數中直接delete掉內存,那麽兩個對象,意味著該內存要被delete兩次。其結果可想而知。

為了解決這個問題,我們引入了引用計數思想。使用引用計數,對擁有字符串資源的對象數目進行計數。只有當擁有該字符串資源的對象數目為0時,才銷毀字符串內存資源。這就能夠保證該內存只被delete一次。

#include <string.h>
#include <iostream>

class string_rep {
    friend class my_string;
public:
    string_rep(const char* str = NULL) : use_count_(1) {
        if(str == NULL){
            str_ = new char[1];
            *str_ = ‘\0‘;
        }   
        else{
            str_ = new char [strlen(str)+1];
            strcpy(str_, str);
        }   
    }   

    //trivial copy assignment

    ~string_rep() {
        delete []str_;
        str_ = NULL;
    }   
public:
    unsigned int use_count() const {
        return use_count_;
    }   
    const char* c_str() const {
        return str_;
    }   
private:
 void increment() {
        ++use_count_;
    }
    void decrement() {
        if(--use_count_ == 0)
            delete this;
    }
private:
    char         *str_;
    unsigned int use_count_;
};

class my_string {
public:
    my_string(const char* str = NULL)
        : rep(new string_rep(str)) {
    }
    my_string(const my_string& other) {
        rep = other.rep;
        rep->increment();
    }
    my_string& operator=(const my_string& other) {
        if(this != &other){
            rep->decrement();  //先減一
            rep = other.rep; 
            rep->increment();
        }
        return *this;
   }
    ~my_string() {
        rep->decrement();  //不要忘記這步
    }
public:
    unsigned int use_count() const {
        return rep->use_count();
    }
    const char* c_str() const {
        return rep->c_str();
    }
    void tupper() {
        if(use_count() > 1){
            string_rep* new_rep = new string_rep(rep->str_);
            rep->decrement();
            rep = new_rep;   //替換操作
        }

        for(char *s=rep->str_; *s!=‘\0‘; ++s)
            *s -= 32;
    }
private:
    string_rep *rep;
};

解決了資源的銷毀問題,那麽還有一個新的問題:多個對象共享一個字符串資源,那麽如果某個對象需要修改字符串,怎麽處理?答案:copy-on-write技術。

copy-on-write技術,簡稱COW技術。意思就是只有在寫資源的時候才拷貝,讀資源並不拷貝。當我們需要修改字符串,這就是寫了。比如上面的tupper()函數,這就是copy-on-write技術在string類的一個簡單實現。修改字符串時,我們直接new出一個新的string_rep類替換舊的,在新的string_rep上操作即可。不過要註意,要保證舊有資源的釋放,否則會造成內存泄漏問題。

在上面的實現中,還有一個技巧,那就是delete this。這可是一個爭議的東西,不過在引用計數中,使用delete this是安全的的。因為引用計數中,就本例來說,string_rep類僅暴露給了my_string類,而delete_this又是引用計數為0導致的,要麽是在my_string類的析構函數中引起,要麽是在copy-on-write函數中引起,這都是安全的。this指針在delete之後並不會暴露給外部。

使用delete this的註意事項:

1.this對象必須是用new操作符分配的(而不是new[],也不是placement new)。
2.dlete this後,不能訪問該對象的任何成員變量及虛函數。因為delete this會銷毀成員原諒以及vptr。但是註意,並不銷毀vtbl,因為vtbl是該類所有對象共有的,如果你知道C++對象模型這就很好理解了,這裏不贅述。
3.delete this後,不能在訪問this指針。

好了,以上這些就是關於string類的一些技巧,不過實際上有很優秀的編程思想蘊含在裏面,慢慢提高吧。

使用引用計數和copy-on_write實現String類