我们知道浅拷贝会带来资源重复释放的问题。因此我们引入了深拷贝。
但是深拷贝也有缺点。对于大资源深拷贝会造成空间的极大浪费。为解决深拷贝问题,我们引入了引用计数的功能。
首先我们实现一个简单的管理字符串的类。
#pragma once
#include // ostream
#include // 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;
};
该类有字符串指针,缓冲区长度和字符串长度共三个成员数据。
在类的拷贝构造函数中,我们进行了深拷贝,在移动构造函数中,我们进行了移动拷贝。析构函数释放内存空间。
上面的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
#include // 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;
}
就是引用计数是静态成员,属于类的。所有类对象共享一个引用计数。如果大家都只使用一个字符串(一个资源),这没问题,一个引用计数只记录这一个资源的使用情况,不会有资源释放时的泄露。
但如果有多个字符串(多个资源),用一个引用计数,这时就会发生资源泄露问题。看下面的代码。
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,释放资源。
引用计数1的问题是资源泄露,原因是所有资源共享一个引用计数。正常的思路应该是为每一个资源分配一个引用计数,一个引用计数管理一个资源,就不会出现资源泄露问题。
在代码实现上,要设计出一个引用计数对应一个资源的方案。显然该引用计数不能为静态成员变量(所有资源共享一个引用计数)。如果将引用计数设为普通成员变量(如int型),那么每个类对象分别有自己的引用计数,该引用计数不能反映该资源的使用情况,因为对该引用计数的修改只能影响该类对象本身,不能影响共享该资源的所有类对象。
解决方案就是增加一个指向引用计数的指针变量,作为类成员变量。当执行构造函数时,表示构造一个新的资源,用该引用计数的指针动态申请4字节空间,用来存放引用计数的值,将引用计数值初始化为0,并加1。在拷贝构造中,将引用计数的指针直接复制过去,并增加引用计数的值。在析构函数中,通过引用计数的指针,找到引用计数的值,来判断是否应该释放内存。这样就完成了一个引用计数对应一个资源。
由于引用计数的空间也是动态申请的,因此当引用计数的值为0时,也需要释放引用计数所占用的空间。
下面是改进的引用计数的实现代码:
#include
#include // 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;
}
引用计数1和2都会有的问题就是,其中一个对象修改了共享资源,则共享该资源的其余的对象访问该资源时,得到的都是被修改以后的资源。像上面的代码中str3修改资源,导致str2访问到的时修改以后的资源。
为了解决这个问题,我们又引入了写拷贝的机制。