浅拷贝、深拷贝、引用计数法、写时拷贝,循序渐进的分析

浅拷贝:

class String
{
public:
    String(const char *pStr = "")
        :_pStr(new char[strlen(pStr) + 1])
    {
        strcpy(_pStr, pStr);
    }
    ~String()
    {
        delete[] _pStr;
        _pStr = NULL;
    }
private:
    char *_pStr;
};
void FunTest()
{
    String s1("hello");
    String s2(s1);
    String s3 = s2;
}
int main()
{
    FunTest();
    system("pause");
    return 0;
}
浅拷贝在运行过程中总是会发生崩溃,为了找出原因进行一步步调试。

发现在通过已有对象创建新的对象通过已有对象给新的对象赋值时,新对象和原来的对象竟然公用着同一段内存。在FunTest()这个函数调用完成之后,返回之前开始销毁局部变量,会发现当第一次调用析构函数之后,s3被销毁,其他对象使用的那段内存也跟着被释放。


这样就导致在接下来 调用析构函数释放其他对象时,去释放一段已经被释放的内存(因为他们公用一块内存)导致程序崩溃。

总结:在浅拷贝中,由于拷贝构造函数和复制运算符重载时,只把已有对象的内容赋给新创建的对象,导致多个对象公用了同一段内存,结果当任意一个对象销毁时他就会释放那段他们公用的内存,当剩下的对象在被销毁时,就回重复的释放那段内存空间,导致程序崩溃。


深拷贝

class String
{
public:
    String(const char *pStr = "")
        :_pStr(new char[strlen(pStr) + 1])
    {
        strcpy(_pStr, pStr);
    }
    String(const String& s)
    {
        _pStr = new char[strlen(s._pStr) + 1];
        strcpy(_pStr, s._pStr);
    }
    String& operator= (String &s)
    {
        if (this != &s)
        {
            String tmp(s);
            std::swap(_pStr, s._pStr);
        }
        return *this;
    }
    ~String()
    {
        delete[] _pStr;
        _pStr = NULL;
    }
private:
    char *_pStr;
};
void FunTest()
{
    String s1("hello");
    String s2(s1);
    String s3 = s2;
}
int main()
{
    FunTest();
    system("pause");
    return 0;
}

对于浅拷贝出现的问题,我们可以考虑这样解决。自己实现拷贝构造函数和赋值操作符的重载,对于不同的对象,给他们开辟出不同的内存用于存放字符串。

这样在对象被销毁时,调用析构函数时,释放的一定是该对象自己的那快内存空间,不会打扰到其他对象的内存,这样就避免了互相之间的干扰,程序正常执行。

可以看到,FunTest()函数调用完成之后,s1,s2,s3三个对象被成功的销毁,程序正常!



引用计数


class String
{
public:
    String(const char *pStr = "")
        :_pStr(new char[strlen(pStr) + 1])
        , count(new int[1])
    {
        strcpy(_pStr, pStr);
        *count = 1;
    }
    String(const String& s)
    {
        _pStr = s._pStr;
        count = s.count;
        (*count)++;
    }
    ~String()
    {
        if (NULL != _pStr && *count == 1)
        {
            delete[] _pStr;
            delete[] count;
            _pStr = NULL;
            count = NULL;
        }
        else if (*count > 1)
        {
            (*count)--;
        }
    }
private:
    char *_pStr;
    int *count;
};
void FunTest()
{
    String s1("hello bit");
    String s2(s1);
}
int main()
{
    FunTest();
    system("pause");
    return 0;
}
对于浅拷贝所出现的问题,不仅仅可以使用深拷贝来解决,我们可以通过标记储存字符串那段空间的使用次数,用来真正确定,这块空间是不是真的要释放!这就是引用计数法

引用计数,它是多个对象一同进行维护的。比如创建s1之后,s1的引用计数就为1,通过s1创建s2,s1和s2共用了s1的字符串。则s1和s2的引用计数要一致为2。为了实现,所以在成员变量定义一个int* 类型的指针,这个指针指向存储引用计数的空间,多个对象指向同一块的引用计数空间时,说明他们使用的同一字符串!

创建对象s1,使用s1拷贝构造s2,可以看到他们使用的是同一字符串,同一引用计数

在函数返回之前,s2先销毁,发现s1也是用的是这块空间,所以就让引用计数减去1,不进行销毁。

不过对于引用计数存在一个很重要的缺点,就是当多个对象使用同一个字符串时,任何一个对象修改字符串里面的值都会造成其他对象所指向的字符串改变!

对于上图,s2的内容被修改之后,s1的内容也随之修改,这就是引用计数的最大不足!


写时拷贝(copy-on-write)

什么是写时拷贝?就是在多个对象共用字符串时,有一个对象需要修改字符串的内容,这个时候把字符串拷贝一份赋给这个对象,以免这个对象去修改其他对象所使用的字符串。
什么时候会发现写时才拷贝?很显然,当然是在共享同一块内存的类发生内容改变时,才会发生Copy-On-Write。比如string类的[]、=、+=、+、操作符赋值,还有一些string类中诸如insert、replace、append等成员函数,包括类的析构时。修改数据才会触发Copy-On-Write。写时拷贝是对引用计数法的一种优化,可以消除引用计数法中的错误!

在实现写时拷贝之前,我们先来对引用计数法进行一下优化,我们之前对引用计数和字符串是分了两块不同的内存存放,在我们实现的过程中不利于实现,所以我们将他们放在同一块内存中,引用计数占内存的前四个字节,后面存放字符串。

实现一个简单的写时拷贝:

class String
{
public:
    String(char *pStr = "")
        :_pStr(new char[strlen(pStr) + 4 + 1])
    {
        _pStr += 4;
        strcpy(_pStr, pStr);
        *(int *)(_pStr - 4) = 1;
    }
    String(const String& s)
    {   
        _pStr = s._pStr;
        int len = Size();
        (*(int *)(_pStr - 4))++;
    }
    ~String()
    {
        if (NULL != _pStr)
        {
            int len = Size();
            if (len == 1)
            {
                delete[](_pStr + 4);
                _pStr = NULL;
            }
            else
            {
                (*(int *)(_pStr - 4))--;
            }
        }
    }
    void WriteCopy()//写时拷贝
    {
        int len = Size();
        char *tmp = new char[len + 1 + 4];
        tmp += 4;
        strcpy(tmp, _pStr);
        if (len > 1)
        {
            (*(int *)(_pStr - 4))--;
            std::swap(_pStr, tmp);
            (*(int *)(_pStr - 4)) = 1;
        }
    }
    size_t Size()const
    {
        char* tmp = _pStr;
        size_t count = 0;
        while (*tmp++)
        {
            count++;
        }
        return count;
    }
    String operator =(const String & s)
    {
        _pStr = s._pStr;
        int len = this->Size();
        (*(int *)(_pStr - 4))++;
    }
    char& operator [](size_t idx)
    {
        static char sNULL = '\0';
        if (idx < Size() && idx > 0)
        {
            WriteCopy();
            return _pStr[idx];
        }
        return sNULL;
    }
    const char& operator [](size_t idx)const
    {
        static char sNULL = '\0';
        if (idx < Size() && idx > 0)
        {
            return _pStr[idx];
        }
        return sNULL;
    }
    
public:
    char *_pStr;
};
void FunTest()
{
    String s1("hello bit");
    cout << *(((int *)s1._pStr) - 1) << endl;
    String s2(s1);
    String s3 = s2;
    cout << *((int *)s1._pStr - 1) << endl;
    s3[2] = 'W';
    cout << *((int *)s1._pStr - 1) << endl;
}
int main()
{
    FunTest();
    system("pause");
    return 0;
}
先创建对象s1

通过s1创建s2


创建s3通过s2赋值给s3

改变s3的内容,这个时候就会发生写时拷贝,s3会拥有新的内存空间

s1引用计数的变化


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值