TestCNBlogs

使用引用计数和 COW 实现 String 类

这算是我开始复习的内容吧,关于 String 类半年前写过,最近拿出来溜溜,免得面试被问到而自己又忘记了。

之前的文章地址:[C++ 引用计数思想--利用引用计数器自定义 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;
};

解决了资源的销毁问题,那么还有一个新的问题:多个对象共享一个字符串资源,那么如果某个对象需要修改字符串,怎么处理?答案:COW 技术。

COW(copy on write) 技术,就是只有在写资源的时候才拷贝,读资源并不拷贝。修改字符串就属于写资源。例如上面的 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类的一些技巧,不过实际上有很优秀的编程思想蕴含在里面,慢慢提高吧。

posted on 2019-02-06 00:06  TestCNBlogss  阅读(414)  评论(0编辑  收藏  举报

导航