C++ String类的引用计数的浅拷贝、写时拷贝

本文通过模拟实现string类的方式,详细介绍了深拷贝、浅拷贝及其潜在问题。进一步探讨了引用计数的浅拷贝实现,并通过逐步改进解决了内存管理和效率问题。最后介绍了写时拷贝技术,以解决共享资源修改时的数据一致性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这篇博客将一步一步写出写实拷贝的的思路,以模拟实现string类为例子

1、深浅拷贝

#include <iostream>
#include <stdlib.h>

class String
{
public:
    String(const char* str = "")
        :_str(new char[strlen(str)+1])
    {
        strcpy(_str, str);
    }

    //浅拷贝
    String(const String&s)
    {
        _str = s._str;
    }
    ////深拷贝
    //String(const String&s)
    //  :_str(new char[strlen(s._str)+1])
    //{
    //  strcpy(_str, s._str);
    //}

    ~String()
    {
        if (_str)
        {
            delete[] _str;
        }
    }

private:
    char*_str;
};

int main()
{
    String s1("123455");
    String s2(s1);
    return 0;
}

上面的程序使用浅拷贝将会崩溃。因为浅拷贝是直接指向了同一块空间,在调用析构函数的时候会对同一块空间delete[]了两次。这样是不合法的。使用深拷贝将会没有任何问题。
这里写图片描述

使用了深拷贝,其实就是另外开辟一块空间,再将原来空间的值拷贝下来。
这里写图片描述

2、引用计数的浅拷贝

如果我们不想用深拷贝,因为深拷贝会开辟空间,这样浪费空间,同时还影响效率。那么我们就可以使用引用计数的浅拷贝。引用计数的浅拷贝指的是当一个类还有对象存在的时候,就不进行析构。这样就避免对同一块空间的多次delete[]。

(1)引入static _refcount当做计数器
class String
{
public:
    String(const char* str = "")
        :_str(new char[strlen(str)+1])
    {
        strcpy(_str, str);
    }

    //浅拷贝
    String(const String&s)
    {
        _str = s._str;
        _refcount++;//进行拷贝构造,增加引用计数的个数
    }

    ~String()
    {
        if (--_refcount == 0)
        {
            delete[] _str;
        }
    }
private:
    char*_str;
    static int _refcount;//定义为静态的
};

int String::_refcount = 1;//静态全局变量需要在类外面初始化

定义为静态的_refcount 是为了让对象共用一个。而不是每一个对象都有一个引用计数。
这样似乎是对的,但是一个类可以同时开辟出多个对象,这多个对象都有自己的空间,但是使用了静态的_refcount的话,他们都共有一个引用计数,这样依然会对同一块空间有多次的delete[]。

这里写图片描述

(2)使用指针类型的_refcount

为了克服以上的问题,我们打算开辟一个整型大小空间,用int* ——refcount维护这块空间。这样就可以保证每一块空间搭配一个引用计数,同时同一个对象拷贝构造出来的使用的是同一个引用计数。

class String
{
public:
    String(const char* str = "")
        :_str(new char[strlen(str) + 1])
        , _refcount(new int(1))
    {
        strcpy(_str, str);
    }

    String(const String& s)
        :_str(s._str)
    {
        _refcount = s._refcount;
        (*_refcount)++;
    }

    //s1 = s2
    String& operator=(const String& s)
    {
        if (_str != s._str)
        {
            if (--(*_refcount) == 0)
            {
                delete[] _str;
                delete _refcount;
                _str = NULL;
                _refcount = NULL;
            }
            _str = s._str;
            _refcount = s._refcount;
            ++(*_refcount);
        }
        return *this;
    }

    ~String()
    {
        if (--(*_refcount) == 0)
        {
            delete[] _str;
            delete _refcount;
            _str = NULL;
            _refcount = NULL;
        }
    }

private:
    char* _str;
    int* _refcount;
};

int main()
{
    String s1("123455");
    String s2(s1);
    String s3(s2);
    //创建新的空间
    String s4("0009");
    String s5(s4);
    return 0;
}      

s1\s2\s3指向同一块空间,共用一个引用计数。s4\s5指向另外一块空间,共用另一个引用计数
这里写图片描述

这里写图片描述

(3)将引用计数放到开辟空间的前面

使用指针的引用计数是一种好的方式,但是这样会增加内存空间的碎片化。为了解决内存碎片化,我们可以在开辟的_str 空间的前面多开一个int大小的空间,用作存放引用计数。模仿new[]的做法。

修改代码如下:

class String
{
public:
    String(const char* str = "")
        //strlrn(str) + 1是给'\0'留下的空间,+4是给引用计数
        :_str(new char[strlen(str) + 1 + 4])
    {
        *((int*)_str) = 1;
        //让_str指向引用计数之后的空间,即字符串的开始
        _str += 4;
        strcpy(_str, str);
    }

    String(const String& s)
        //让_str指向引用计数
        :_str(s._str-4)
    {
        //引用计数加一
        (*((int*)_str))++;
        _str += 4;
    }

    //s1 = s2
    String& operator=(const String& s)
    {
        if (_str != s._str)
        {
            //引用计数减一判0
            if (--(*((int*)_str-1)) == 0)
            {
                delete[] _str;
                _str = NULL;
            }
            _str = s._str;
            //引用计数加一
            (*((int*)s._str - 1))++;
        }
        return *this;
    }

    ~String()
    {
        if (--(*((int*)_str - 1)) == 0)
        {
            delete[] _str;
            _str = NULL;
        }
    }

private:
    char* _str;
};

int main()
{
    String s1("123455");
    String s2(s1);
    String s3(s2);

    String s4("0009");
    String s5(s4);
    return 0;
}      

这里写图片描述

这里写图片描述

至此,我们的引用计数的浅拷贝已经完成的很好了。

3、写时拷贝

有一个问题,因为s1、s2同时指向了同一块空间,如果我对s1进行了值的修改,必定会同时修改了s2的值。我们不希望这事情的发生,因此我们引入了写时拷贝。写时拷贝的意思是,当我需要向空间写入值的时候,即修改值的时候,我们就将修改的对象拷贝一份出来。这样就不会影响到其他的对象了。

void CopyOnWrite()
{
    if (*((int*)_str-1) > 1)
    {
        (*((int*)_str - 1))--;
        char* tmp = new char[strlen(_str) + 5];
        tmp -= 4;
        strcpy(tmp, _str);
        _str = tmp;
        (*((int*)_str+1))++;
    }
}

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值