c++实现string的写时复制技术(COW)——注释超细版

c++实现string的写时复制技术(COW)——注释超细版

COW(copy-on-write

前言

当字符串进行复制的时候,如很长的字符串(2k大小),如果全部采用堆空间存储的话那是非常浪费空间的,复制一次变成4k大小,两次6k…

所以为了节约空间,在两个字符串都是相同内容的时候,将复制后的指针指向原始字符串的地址空间,然后采用引用计数的方式对该空间的引用数+1;当修改某一指针的内容时,其实就是改变了字符串数据,那么显然变成了一个新的字符串,这时候就需要给这个新的字符串重新申请一块堆空间了,然后对原始字符串的引用计数-1。

代码实现
#include <iostream>
#include <string.h>

using namespace std;

class String
{
private:
    char *_pstr;

public:
    String()
        // 申请5个字节的堆空间(4个字节的int类型的引用计数,一个字节字符串'\0'
        // 向后偏移四个字节的空间(越过引用计数直接到字符串)
        : _pstr(new char[5]() + 4)
    {
        // static_cast不能用于两个明确类型的指针进行转换,可以使用reinterpret_cast转换
        // *static_cast<int *>(_pstr - 4) = 1;      // error
        // *reinterpret_cast<int *>(_pstr - 4) = 1; // ok, 但是不安全
        *(int *)(_pstr - 4) = 1; // 指针偏移到引用计数位,初始值为1
        cout << "String()" << endl;
    }

    // 有参构造函数
    String(const char *pstr)
        : _pstr(new char[strlen(pstr) + 5]() + 4)
    {
        strcpy(_pstr, pstr);
        *(int *)(_pstr - 4) = 1;
        cout << "String(const char *pstr)" << endl;
    }

    // 拷贝构造函数,由于内容一样,所以采用浅拷贝,两个指针指向同一个空间,
    // 增加引用计数值,不重新申请内存空间
    String(const String &rhs)
        // 指针指向同一块堆空间
        : _pstr(rhs._pstr)
    {
        ++*(int *)(_pstr - 4); // 拷贝构造函数,引用计数+1
        cout << "String(const String &rhs)" << endl;
    }

    // 赋值运算符函数,还是相同的字符串,所以采用浅拷贝
    String &operator=(const String &rhs)
    {
        cout << "String operator=(const String &rhs)" << endl;
        // 判断是否是自己赋值给自己
        if (this != &rhs)
        {
            // 如果被赋值的字符串引用计数为1,那么就释放(被赋值的)原始内存空间,
            // 防止脏数据(如被赋值的数据长度大于赋值过来的数据)
            if (1 == *(int *)(_pstr - 4))
            {
                delete[](_pstr - 4);
            }

            // 两个指针指向同一个字符串,实现浅拷贝
            _pstr = rhs._pstr;
            // 对字符串引用计数+1
            ++*(int *)(_pstr - 4);
        }
        // 返回字符串
        return *this;
    }

    // 析构函数
    ~String()
    {

        // 当引用计数为1时,表明没有两个指针同时指向一块内存空间,可以直接删除
        if (1 == *(int *)(_pstr - 4))
        {
            delete[](_pstr - 4);
            _pstr = nullptr;
        }
        cout << "~String()" << endl;
    }

    class charProxy
    {
    private:
        String &_self;
        size_t _idx;

    public:
        // 需要修改数据,所以参数应该有需要修改的字符串、下标
        charProxy(String &self, size_t idx)
            : _self(self), _idx(idx) {}

        // 实现下标写操作
        char &operator=(const char &rhs)
        {
            cout << "char &operator=(const char &rhs)" << endl;
            // 判断下标合法性
            if (_idx < _self.size() && _idx >= 0)
            {
                // 如果是共享的,此时需要修改数据了所以需要申请一块新的堆空间
                if (_self.refCount() > 1)
                {
                    char *ptmp = new char[_self.size() + 5]() + 4;
                    strcpy(ptmp, _self._pstr);

                    // 由于分配了新的空间所以引用计数-1
                    --*(int *)(_self._pstr - 4);

                    // 将新的字符串的指针赋值给_pstr,这样才可以传出去
                    _self._pstr = ptmp;

                    // 改写后新的字符串的引用计数变为1
                    *(int *)(_self._pstr - 4) = 1;
                }
                // 真正赋值操作
                _self._pstr[_idx] = rhs;
                // 返回修改后的值
                return _self._pstr[_idx];
            }
            else
            {
                cout << "error idx" << endl;
                static char nullchar = '\0';
                return nullchar;
            }
        }

        // 隐式转换:将charProxy转换为char类型,免去重载新的输出流运算符
        operator char()
        {
            return _self._pstr[_idx];
        }
    };

    // 重载下标访问运算符,返回类型为charProxy类,为了区分对数据的读操作与写操作,
    // 如果写成char &operator[](size_t idx)这种,没有办法区分读下标操作还是写下标操作,
    // 如果返回类型为charProxy,那么在进行赋值操作的时候会触发charProxy的赋值运算符函数
    // 这样就去分了string的读写操作,单独读下标是不会触发charProxy的赋值运算符函数的
    charProxy operator[](size_t idx)
    {
        return charProxy(*this, idx);
    }

    // 字符串大小获取函数
    size_t size() const
    {
        return strlen(_pstr);
    }

    // 引用计数值获取函数
    int refCount() const
    {
        return *(int *)(_pstr - 4);
    }

    const char *strAddr() const
    {
        return _pstr;
    }

    // 重载输出流运算符,输出自定义类型
    friend ostream &operator<<(ostream &os, const String &rhs);
};

ostream &operator<<(ostream &os, const String &rhs)
{
    if (rhs._pstr)
    {
        os << rhs._pstr;
    }
    return os;
}

int main(int argc, char **argv)
{
    String s1("hello");
    cout << "s1 = " << s1 << endl;
    cout << "s1.refcount = " << s1.refCount() << endl;
    printf("s1.addr = %p\n", s1.strAddr());

    cout << endl;
    cout << "拷贝操作String s2 = s1" << endl;
    String s2 = s1;
    cout << "s1 = " << s1 << endl;
    cout << "s2 = " << s2 << endl;
    cout << "s1.refcount = " << s1.refCount() << endl;
    printf("s1.addr = %p\n", s1.strAddr());
    cout << "s2.refcount = " << s2.refCount() << endl;
    printf("s2.addr = %p\n", s2.strAddr());
    s2.strAddr();

    cout << endl;
    cout << "赋值操作s3 = s2" << endl;
    String s3;
    s3 = s2;
    cout << "s2.refcount = " << s2.refCount() << endl;
    printf("s2.addr = %p\n", s2.strAddr());
    cout << "s3.refcount = " << s3.refCount() << endl;
    printf("s3.addr = %p\n", s3.strAddr());

    cout << endl;
    cout << "读下标操作" << endl;
    cout << "s2[0] = " << s2[0] << ", s1 = " << s1 << endl;
    cout << "s1.refcount = " << s1.refCount() << endl;
    printf("s1.addr = %p\n", s1.strAddr());
    cout << "s2.refcount = " << s2.refCount() << endl;
    printf("s2.addr = %p\n", s2.strAddr());

    cout << endl;
    cout << "对数据写操作,将会触发写时复制技术!" << endl;
    s2[0] = 'H';
    cout << "s2[0] = " << s2[0] << ", s2 = " << s2 << endl;
    cout << "s1.refcount = " << s1.refCount() << endl;
    printf("s1.addr = %p\n", s1.strAddr());
    cout << "s2.refcount = " << s2.refCount() << endl;
    printf("s2.addr = %p\n", s2.strAddr());
    cout << "s3.refcount = " << s3.refCount() << endl;
    printf("s3.addr = %p\n", s3.strAddr());

    return 0;
}
运行结果

注意看内存地址变化,及引用计数的变化

String(const char *pstr)
s1 = hello
s1.refcount = 1
s1.addr = 0x55d2299c0e74

拷贝操作String s2 = s1
String(const String &rhs)
s1 = hello
s2 = hello
s1.refcount = 2
s1.addr = 0x55d2299c0e74
s2.refcount = 2
s2.addr = 0x55d2299c0e74

赋值操作s3 = s2
String()
String operator=(const String &rhs)
s2.refcount = 3
s2.addr = 0x55d2299c0e74
s3.refcount = 3
s3.addr = 0x55d2299c0e74

读下标操作
s2[0] = h, s1 = hello
s1.refcount = 3
s1.addr = 0x55d2299c0e74
s2.refcount = 3
s2.addr = 0x55d2299c0e74

对数据写操作,将会触发写时复制技术!
char &operator=(const char &rhs)
s2[0] = H, s2 = Hello
s1.refcount = 2
s1.addr = 0x55d2299c0e74
s2.refcount = 1
s2.addr = 0x55d2299c12a4
s3.refcount = 2
s3.addr = 0x55d2299c0e74
~String()
~String()
~String()

如果本文对你有帮助,记得一键三连哦,一键三连笑哈哈,代码能力顶呱呱!

本人能力有限,如有错误,望不吝指正;原创不易,欢迎转载,转载请注明出处!

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
C++中的string类是一个用于处理字符串的标准库类。它提供了一系列成员函数和操作符重载,使得字符串的操作更加方便和高效。 C++中的string类位于命名空间std中,因此在使用之前需要包含头文件< string >。 下面是一个简单的示例,展示了如何使用string类来创建、初始化和操作字符串: ```cpp #include <iostream> #include <string> int main() { // 创建一个空字符串 std::string str; // 初始化字符串 std::string greeting = "Hello, world!"; // 获取字符串长度 int length = greeting.length(); std::cout << "Length: " << length << std::endl; // 连接字符串 std::string name = "Alice"; std::string message = greeting + " My name is " + name; std::cout << "Message: " << message << std::endl; // 获取子串 std::string substring = message.substr(7, 5); std::cout << "Substring: " << substring << std::endl; // 查找子串 size_t position = message.find("world"); if (position != std::string::npos) { std::cout << "Found at position: " << position << std::endl; } else { std::cout << "Not found" << std::endl; } return 0; } ``` 上述示例中,我们首先创建了一个空字符串`str`,然后使用赋值运算符将字符串"Hello, world!"赋给了变量`greeting`。接着,我们使用`length()`函数获取了字符串的长度,并使用`+`运算符将多个字符串连接起来形成新的字符串。我们还使用`substr()`函数获取了字符串的子串,并使用`find()`函数查找了子串在原字符串中的位置。 除了上述示例中的操作,string类还提供了许多其他有用的成员函数和操作符重载,如插入、删除、替换、比较等。你可以参考C++的官方文档或其他相关资料来了解更多关于string类的详信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值