c++中string的cow机制以及其为何在cpp11后不再被支持

本文起因

今天学习《Effective STL》的时候看到条款15,里面提到了有些stl的string实现里可能会有RefCount字段做引用计数,在实验相关内容后发现该字段已经在c++11中被废弃,为了琢磨明白为什么会废弃,进行了一番搜索,最后整理为本文。

RefCount介绍

书中介绍的一种string实现如下图。

其中RefCount字段是为了在copy string对象时避免直接重新分配内存,并拷贝字符串数据而设计的。比如在下面这段代码中,str2在赋值时拷贝了str1的数据,但实际上str2和str1都没有修改数据的行为,因此他们可以通过共用一块内存区域来节省内存占用以及节省分配时间。

#include <string>
int main()
{
    std::string str1 = "hello world\n";
    auto str2 = str1;
    printf(str1.c_str());
    printf(str2.c_str());
    return 0;
}

如果采用了共享内存的策略,那么当str1或者str2修改了字符串的内容时,岂不是会导致另一个字符串的数据被同步修改了?此时RefCount的作用就体现出来了,这个字段是配合copy on write(COW,写时拷贝)技术使用的。

COW简单来说就是在复制对象时并不进行内存的拷贝,而是当这些共享内存的对象中有一个被写入时,再进行拷贝,以避免其他共享内存的对象被影响。判断是否有其他对象在共享内存的依据就是RefCount字段,当发生复制对象的行为时,引用计数加1,当发生修改对象行为时,如果引用计数大于0,则将引用计数减一并且重新分配内存。大概的代码如下,本人按理解手写的,并没有考虑多线程时的资源竞争情况。

class MyString
{
    struct stringPointer
    {
        unsigned int refCount = 0;
        char *strPointer;
        unsigned int strLen = 0;
    };

public:

    MyString(char * str)
    {
        m_ptr = new stringPointer();
        m_ptr->strLen = strlen(str);
        m_ptr->strPointer = new char[m_ptr->strLen];
        std::memcpy(m_ptr->strPointer, str, m_ptr->strLen);
    }

    MyString(const MyString & str)
    {
        m_ptr = str.m_ptr;
        m_ptr->refCount++;
    }

    char * c_str()
    {
        return m_ptr->strPointer;
    }

    char& operator[](unsigned int index)
    {
        if(m_ptr->refCount > 0)
        {
            //实现拷贝操作
            m_ptr->refCount--;
            stringPointer* tempStr = new stringPointer();
            tempStr->strLen = m_ptr->strLen;
            tempStr->strPointer = new char[m_ptr->strLen];
            std::memcpy(tempStr->strPointer, m_ptr->strPointer, m_ptr->strLen);
            m_ptr = tempStr;
        }
        return (m_ptr->strPointer)[index];
    }

    ~MyString()
    {
        if(m_ptr->refCount == 0)
        {
            delete [] m_ptr->strPointer;
            delete m_ptr;
        }
        else
        {
            m_ptr->refCount--;
        }

    }

private:
    stringPointer * m_ptr;
};

int main()
{
    MyString str1("hello world\n");
    auto str2( str1);
    
//输出两字符串的内容及地址
    printf(str1.c_str());
    printf(str2.c_str());
    printf("str1 address is %x\n",str1.c_str());
    printf("str2 address is %x\n",str2.c_str());

//修改str1后再输出两字符串的内容及地址
    str1[0] = 'y';
    printf(str1.c_str());
    printf(str2.c_str());
    printf("str1 address is %x\n",str1.c_str());
    printf("str2 address is %x\n",str2.c_str());

//修改str2后再输出两字符串的内容及地址
    str2[0] = 'w';
    printf(str1.c_str());
    printf(str2.c_str());
    printf("str1 address is %x\n",str1.c_str());
    printf("str2 address is %x\n",str2.c_str());

    return 0;
}

实现结果如下:

可以发现,在没有写入时,str1和str2共享了内存,在写入str1时,为str1重新分配了内存,之后再写入str2,因为引用计数为0,所以并不需要再次重新分配内存,只需要在原地修改即可。

hello world
hello world
str1 address is a5405b00
str2 address is a5405b00
yello world
hello world
str1 address is a5405b30
str2 address is a5405b00
yello world
wello world
str1 address is a5405b30
str2 address is a5405b00

标准库中的string实验

实际上我并没有一上来就自己定义一个MyString来进行实验,毕竟我是来学习STL怎么用的,不是来造轮子的。本来我是直接用STL中的string进行实验的,但是实验的结果并没有符合我的预期,实验代码及结果如下:

#include <string>
int main()
{
//    MyString str1("hello world\n");
//    auto str2( str1);
    std::string str1("hello world\n");
    auto str2( str1);
    printf(str1.c_str());
    printf(str2.c_str());
    printf("str1 address is %x\n",str1.c_str());
    printf("str2 address is %x\n",str2.c_str());

    str1[0] = 'y';
    printf(str1.c_str());
    printf(str2.c_str());
    printf("str1 address is %x\n",str1.c_str());
    printf("str2 address is %x\n",str2.c_str());
    return 0;
}

实验结果:

hello world
hello world
str1 address is e014e541
str2 address is e014e529
yello world
hello world
str1 address is e014e541
str2 address is e014e529

可以发现str1和str2两者一开始就并没有共享内存,也没有在写入内存时进行复制操作。

废弃string中的COW的原因

看书看了半天就学了这么一个知识点,结果还不对,这就很尴尬了,于是我又在网上搜了搜,发现是因为在c++11以后,string就明确不再支持cow机制了,因为这种机制在多线程时会有更大的消耗,并且这种机制会引入bug,如下代码所示,产生bug的原因已经在注释中写明:

#include <string>
int main()
{
//    MyString str1("hello world\n");
//    auto str2( str1);
    std::string str1("hello world\n");
    //p指向str1的数据区
    const char * p = str1.data();
    {
        //str2和str1共享数据
        auto str2( str1);
        //访问导致str1复制数据,此时p和str2直线同一块区域
        str1[0];
        //str2离开作用域,调用析构函数,此时str2的RefCount为0,因此str2指向的内存被释放
    }
    //此时p为野指针,这是严重的bug
    printf(p);
    return 0;
}

c++11中不允许出现这种bug,所以在标准中禁止了string的cow策略。

本文就到此结束了,虽然整了半天就得出一个没啥意义的结论,但是多少提高了自己对cpp的认识,也算是有点好处吧。好久没写博客了,没成想一写就花了一个小时,如果有人读到这篇博客,希望能对你有帮助,不然就是浪费了我们彼此的时间。

参考链接

https://stackoverflow.com/questions/12199710/legality-of-cow-stdstring-implementation-in-c11

https://blog.csdn.net/u012501054/article/details/90241124

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值