C++引用计数

前言

我们知道浅拷贝会带来资源重复释放的问题。因此我们引入了深拷贝。
但是深拷贝也有缺点。对于大资源深拷贝会造成空间的极大浪费。为解决深拷贝问题,我们引入了引用计数的功能。


0x00 一个简单的String类

首先我们实现一个简单的管理字符串的类。

#pragma once
#include <iostream>  // ostream
#include <stdint.h>  // uint32_t
using namespace std;

class CAString
{
public:
    /************************************************************************/
    /* 构造函数                                                              */
    /************************************************************************/
    CAString()
    {
        m_pBuff = new char[1];
        m_pBuff[0] = '\0';
        m_nBuffSize = 1;
        m_nLength = 0;
    }
    CAString(const char* str)
    {
        m_nLength = strlen(str);
        m_nBuffSize = m_nLength + 1;
        m_pBuff = new char[m_nBuffSize];
        strcpy_s(m_pBuff, m_nBuffSize, str);

    }
    CAString(const CAString& obj) // 拷贝构造 深拷贝
    {
        m_nLength = obj.m_nLength;
        m_nBuffSize = obj.m_nBuffSize;
        m_pBuff = new char[m_nBuffSize];
        strcpy_s(m_pBuff, m_nBuffSize, obj.m_pBuff);

    }
    CAString(CAString&& obj) // 移动构造 移动拷贝
    {

        swap(m_pBuff, obj.m_pBuff);
        swap(m_nLength, obj.m_nLength);
        swap(m_nBuffSize, obj.m_nBuffSize);
    }
    /************************************************************************/
    /* 析构函数                                                              */
    /************************************************************************/
    ~CAString()
    {
        if (m_pBuff != nullptr)
        {
            delete m_pBuff;
            m_pBuff = nullptr;
        }
    }

    /************************************************************************/
    /* 外部使用的获取字符串的接口                                           */
    /************************************************************************/
    const char* GetStringBuffer()
    {
        return m_pBuff;
    }
    
    // 修改字符串的一个字符
    void SetAt(int nIndex, char ch)
    {
        m_pBuff[nIndex] = ch;
    }
public:
    /************************************************************************/
    /* 重载输出运算符                                                        */
    /************************************************************************/
    // 使用全局函数重载 因为要访问到私有数据 因此定义为友元全局函数
    friend ostream& operator<<(ostream& os, const CAString& obj)
    {
        os << obj.m_pBuff;
        return os;
    }
private:
    uint32_t m_nBuffSize;
    uint32_t m_nLength;
    char* m_pBuff;
};

该类有字符串指针,缓冲区长度和字符串长度共三个成员数据。
在类的拷贝构造函数中,我们进行了深拷贝,在移动构造函数中,我们进行了移动拷贝。析构函数释放内存空间。

0x01 引用计数1(解决浅拷贝与深拷贝的问题)

上面的CAString类符合大多数类的设计过程,因为字符串毕竟占用的资源比较少。但如果这里使用的资源很大,例如高帧率电影等。我们在拷贝构造函数中再进行深拷贝就特别占用空间。

为了解决这个问题,显然我们不能再进行深拷贝了,这里我们想到了对资源增加一个引用计数。再构造函数中,引用计数加1,拷贝构造发生时,引用计数加1。析构函数中引用计数减1,如果引用计数为0才会释放资源。这样就不会有浅拷贝带来的资源重复释放问题。也比深拷贝节省内存空间。

为了从代码上实现引用计数,由于这里的引用计数是所有类共享的,因此引用计数我们定义为静态成员变量,并在类外进行初始化。

class CAString
{
	// ...
    // 引用计数
    static uint32_t m_pRefCnt;
};

uint32_t CAString::m_pRefCnt = 0;

为了更简单的操作引用计数,我们提供了操作引用计数的成员函数。

class CAString
{
	// ...
private:
    /************************************************************************/
    /* 对引用计数操作的方法                                                   */
    /************************************************************************/
    uint32_t GetRefCnt()
    {
        return m_pRefCnt;
    }
    void AddRefCnt()
    {
        m_pRefCnt++;
    }
    void SubRefCnt()
    {
        m_pRefCnt--;
    }
};

很显然,我们要在构造函数中递增引用计数。并且在析构函数中递减引用计数,在引用计数为0时,释放该资源。

#include <iostream>
#include <stdint.h>  // uint32_t
using namespace std;



class CAString
{
public:
    /************************************************************************/
    /* 构造函数                                                              */
    /************************************************************************/
    CAString()
    {
        m_pBuff = new char[1];
        m_pBuff[0] = '\0';
        m_nBuffSize = 1;
        m_nLength = 0;

        // 构造函数中引用计数加1
        AddRefCnt();
    }
    CAString(const char* str)
    {
        m_nLength = strlen(str);
        m_nBuffSize = m_nLength + 1;
        m_pBuff = new char[m_nBuffSize];
        strcpy_s(m_pBuff, m_nBuffSize, str);

        // 构造函数中引用计数加1
        AddRefCnt();
    }
    CAString(const CAString& obj) // 拷贝构造
    {
        // 深拷贝
        /*
        m_nLength = obj.m_nLength;
        m_nBuffSize = obj.m_nBuffSize;
        m_pBuff = new char[m_nBuffSize];
        strcpy_s(m_pBuff, m_nBuffSize, obj.m_pBuff);
        */

        // 浅拷贝
        m_pBuff = obj.m_pBuff;
        m_nLength = obj.m_nLength;
        m_nBuffSize = obj.m_nBuffSize;

        // 构造函数中引用计数加1
        AddRefCnt();
    }
    CAString(CAString&& obj) // 移动构造 移动拷贝
    {
        swap(m_pBuff, obj.m_pBuff);
        swap(m_nLength, obj.m_nLength);
        swap(m_nBuffSize, obj.m_nBuffSize);
    }
    /************************************************************************/
    /* 析构函数                                                              */
    /************************************************************************/
    ~CAString()
    {
        // 析构函数中引用计数减1
        SubRefCnt();

        // 如果引用计数为0,就释放资源
        if (GetRefCnt() == 0)
        {
            if (m_pBuff != nullptr)
            {
                delete m_pBuff;
                m_pBuff = nullptr;
            }
        }
    }

    /************************************************************************/
    /* 外部使用的获取字符串的接口                                           */
    /************************************************************************/
    const char* GetStringBuffer()
    {
        return m_pBuff;
    }


    // 修改字符串的一个字符
    void SetAt(int nIndex, char ch)
    {
        m_pBuff[nIndex] = ch;
    }
public:
    /************************************************************************/
    /* 重载输出运算符                                                        */
    /************************************************************************/
    // 使用全局函数重载 因为要访问到私有数据 因此定义为友元全局函数
    friend ostream& operator<<(ostream& os, const CAString& obj)
    {
        os << obj.m_pBuff;
        return os;
    }
private:
    /************************************************************************/
    /* 对引用计数操作的方法                                                   */
    /************************************************************************/
    uint32_t GetRefCnt()
    {
        return m_pRefCnt;
    }
    void AddRefCnt()
    {
        m_pRefCnt++;
    }
    void SubRefCnt()
    {
        m_pRefCnt--;
    }
private:
    uint32_t m_nBuffSize;
    uint32_t m_nLength;
    char* m_pBuff;

    // 引用计数
    static uint32_t m_pRefCnt;
};

uint32_t CAString::m_pRefCnt = 0;

int main()
{
    CAString str2("str2");
    CAString str3(str2);

    //str3.SetAt(0, 'a');

    cout << "str2:" << str2 << endl;
    cout << "str3:" << str3 << endl;

    return 0;
}

0x02 引用计数1的问题

就是引用计数是静态成员,属于类的。所有类对象共享一个引用计数。如果大家都只使用一个字符串(一个资源),这没问题,一个引用计数只记录这一个资源的使用情况,不会有资源释放时的泄露。

但如果有多个字符串(多个资源),用一个引用计数,这时就会发生资源泄露问题。看下面的代码。

int main()
{
    CAString str1("str1");
    CAString str2("str2");
    CAString str3(str2);

    str3.SetAt(0, 'a');

    cout << "str1:" << str1 << endl;
    cout << "str2:" << str2 << endl;
    cout << "str3:" << str3 << endl;

    return 0;
}

构造时,str1和str2执行构造函数,引用计数变为2,str3执行拷贝构造,引用计数变为3。

析构时,str3先析构,引用计数变为2,不释放资源。然后str2析构,引用计数变为1,不释放资源。事实上这里的字符串 “str2” 已经没有引用,本应该释放资源,但引用计数没有为0,因此在这里发生了资源的泄露。最后str1析构,引用计数为0,释放资源。

0x03 引用计数2(一个资源一个引用计数)

引用计数1的问题是资源泄露,原因是所有资源共享一个引用计数。正常的思路应该是为每一个资源分配一个引用计数,一个引用计数管理一个资源,就不会出现资源泄露问题。

在代码实现上,要设计出一个引用计数对应一个资源的方案。显然该引用计数不能为静态成员变量(所有资源共享一个引用计数)。如果将引用计数设为普通成员变量(如int型),那么每个类对象分别有自己的引用计数,该引用计数不能反映该资源的使用情况,因为对该引用计数的修改只能影响该类对象本身,不能影响共享该资源的所有类对象。

解决方案就是增加一个指向引用计数的指针变量,作为类成员变量。当执行构造函数时,表示构造一个新的资源,用该引用计数的指针动态申请4字节空间,用来存放引用计数的值,将引用计数值初始化为0,并加1。在拷贝构造中,将引用计数的指针直接复制过去,并增加引用计数的值。在析构函数中,通过引用计数的指针,找到引用计数的值,来判断是否应该释放内存。这样就完成了一个引用计数对应一个资源。

由于引用计数的空间也是动态申请的,因此当引用计数的值为0时,也需要释放引用计数所占用的空间。

下面是改进的引用计数的实现代码:

#include <iostream>
#include <stdint.h>  // uint32_t
using namespace std;

class CAString
{
public:
    /************************************************************************/
    /* 构造函数                                                              */
    /************************************************************************/
    CAString()
    {
        m_pBuff = new char[1];
        m_pBuff[0] = '\0';
        m_nBuffSize = 1;
        m_nLength = 0;

        // 申请引用计数的空间并初始化为0
        m_pRefCnt = new uint32_t(0);

        // 构造函数中引用计数加1
        AddRefCnt();
    }
    CAString(const char* str)
    {
        m_nLength = strlen(str);
        m_nBuffSize = m_nLength + 1;
        m_pBuff = new char[m_nBuffSize];
        strcpy_s(m_pBuff, m_nBuffSize, str);

        // 申请引用计数的空间并初始化为0
        m_pRefCnt = new uint32_t(0);

        // 构造函数中引用计数加1
        AddRefCnt();
    }
    CAString(const CAString& obj) // 拷贝构造
    {
        // 深拷贝
        /*
        m_nLength = obj.m_nLength;
        m_nBuffSize = obj.m_nBuffSize;
        m_pBuff = new char[m_nBuffSize];
        strcpy_s(m_pBuff, m_nBuffSize, obj.m_pBuff);
        */

        // 浅拷贝
        m_pBuff = obj.m_pBuff;
        m_nLength = obj.m_nLength;
        m_nBuffSize = obj.m_nBuffSize;

        // 拷贝 引用计数的指针
        m_pRefCnt = obj.m_pRefCnt;


        // 构造函数中引用计数加1
        AddRefCnt();
    }
    CAString(CAString&& obj) // 移动构造 移动拷贝
    {
        swap(m_pBuff, obj.m_pBuff);
        swap(m_nLength, obj.m_nLength);
        swap(m_nBuffSize, obj.m_nBuffSize);
        swap(m_pRefCnt, obj.m_pRefCnt);
    }
    /************************************************************************/
    /* 析构函数                                                              */
    /************************************************************************/
    ~CAString()
    {
        // 析构函数中引用计数减1
        SubRefCnt();

        // 如果引用计数为0,就释放资源
        if (GetRefCnt() == 0)
        {

            if (m_pBuff != nullptr)
            {
                // 释放字符串资源
                delete m_pBuff;
                m_pBuff = nullptr;
            }
            if (m_pRefCnt != nullptr)
            {
                // 释放引用计数的资源
                delete m_pRefCnt;
                m_pRefCnt = nullptr;
            }
        }
    }

    /************************************************************************/
    /* 外部使用的获取字符串的接口                                           */
    /************************************************************************/
    const char* GetStringBuffer()
    {
        return m_pBuff;
    }


    // 修改字符串的一个字符
    void SetAt(int nIndex, char ch)
    {
        m_pBuff[nIndex] = ch;
    }
public:
    /************************************************************************/
    /* 重载输出运算符                                                        */
    /************************************************************************/
    // 使用全局函数重载 因为要访问到私有数据 因此定义为友元全局函数
    friend ostream& operator<<(ostream& os, const CAString& obj)
    {
        os << obj.m_pBuff;
        return os;
    }

private:
    /************************************************************************/
    /* 对引用计数操作的方法                                                   */
    /************************************************************************/
    uint32_t GetRefCnt()
    {
        return *m_pRefCnt;
    }
    void AddRefCnt()
    {
        *m_pRefCnt++;
    }
    void SubRefCnt()
    {
        *m_pRefCnt--;
    }


private:
    uint32_t m_nBuffSize;
    uint32_t m_nLength;
    char* m_pBuff;

    // 引用计数指针
    uint32_t* m_pRefCnt;
};

int main()
{
    CAString str1("str1");
    CAString str2("str2");
    CAString str3(str2);

    str3.SetAt(0, 'a');

    cout << "str1:" << str1 << endl;
    cout << "str2:" << str2 << endl;
    cout << "str3:" << str3 << endl;

    return 0;
}

0x04 引用计数带来的问题

引用计数1和2都会有的问题就是,其中一个对象修改了共享资源,则共享该资源的其余的对象访问该资源时,得到的都是被修改以后的资源。像上面的代码中str3修改资源,导致str2访问到的时修改以后的资源。

为了解决这个问题,我们又引入了写拷贝的机制。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shlyyy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值