前言
Write On Copy
需要先看一下 C++引用计数 的内容
0x00 为什么会有写拷贝
因为浅拷贝会重复释放资源,因此C++用深拷贝来解决浅拷贝的这个问题。
因为深拷贝会带来资源占用空间太大的问题,因此C++引入了引用计数的方法解决了深拷贝的这个问题,也顺便解决了浅拷贝重复释放资源的问题。
引用计数虽然好用,但资源毕竟只有一份,如果一个人修改了资源,则其他人访问到的资源都是修改以后的资源。
为了解决引用计数遗留的问题,C++又引入了写拷贝的方法,在引用计数的基础上,增加写时拷贝的功能,当资源被修改时,会先拷贝一个资源的副本,然后对副本进行修改,这样其他的人访问的资源依旧是原来的资源,就不会有因资源修改而导致其它对象访问到修改后的资源的问题。
0x01 写拷贝的实现
在所有修改资源的地方,增加写时拷贝。如果没有修改资源,则不需要进行写拷贝。
需要注意的是,只有资源的引用计数大于1时,才应该进行写拷贝。也就是说引用计数为1只有一个人使用,没必要进行写拷贝。
下面的代码是在 C++引用计数 的基础上进行修改的。
我们把写拷贝封装成私有成员函数:
void WriteOnCopy()
{
// 引用计数大于1时才会进行写拷贝
if (GetRefCnt() > 1)
{
// 原来的引用计数减1
SubRefCnt();
// 拷贝原来的数据
char* pNewBuf = new char[m_nBuffSize];
strcpy_s(pNewBuf, m_nBuffSize, m_pBuff);
m_pBuff = pNewBuf;
// 新的引用计数
uint32_t* pNewRefCount = new uint32_t(0);
m_pRefCnt = pNewRefCount;
// 新的引用计数加1
AddRefCnt();
cout << "Write On Copy" << endl;
}
}
在有对资源修改的地方我们调用写拷贝函数:
// 修改字符串的一个字符
void SetAt(int nIndex, char ch)
{
WriteOnCopy(); // 修改资源时 先调用写拷贝
m_pBuff[nIndex] = ch;
}
完整代码:
#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)
{
WriteOnCopy(); // 修改资源时 先调用写拷贝
m_pBuff[nIndex] = ch;
}
public:
/************************************************************************/
/* 重载输出运算符 */
/************************************************************************/
// 使用全局函数重载 因为要访问到私有数据 因此定义为友元全局函数
friend ostream& operator<<(ostream& os, const CAString& obj)
{
os << obj.m_pBuff;
return os;
}
private:
void WriteOnCopy()
{
// 引用计数大于1时才会进行写拷贝
if (GetRefCnt() > 1)
{
// 原来的引用计数减1
SubRefCnt();
// 拷贝原来的数据
char* pNewBuf = new char[m_nBuffSize];
strcpy_s(pNewBuf, m_nBuffSize, m_pBuff);
m_pBuff = pNewBuf;
uint32_t* pNewRefCount = new uint32_t(0); // 新的引用计数
m_pRefCnt = pNewRefCount;
cout << "Write On Copy" << endl;
// 新的引用计数加1
AddRefCnt();
}
}
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;
}
0x02 写拷贝需要注意的问题
写拷贝是在修改资源时才会发生。
// 修改字符串的一个字符
void SetAt(int nIndex, char ch)
{
WriteOnCopy(); // 修改资源时 先调用写拷贝
m_pBuff[nIndex] = ch;
}
有些函数可能根据条件修改,如果条件都不满足,就不需要修改。并且一个函数应该最多只写拷贝一次。(在函数实现上,加上一个标志位,并且在真正修改资源的地方才进行写拷贝。)
比如下面小写转换为大写的成员函数:
void ToUpperCase()
{
bool bIsUpdated = false;
for (int i = 0; i < m_nLength; i++)
{
if (m_pBuff[i] >= 'a' && m_pBuff[i] <= 'z')
{
// 真正要修改时才进行写拷贝 且写拷贝一次
if (!bIsUpdated)
{
WriteOnCopy();
bIsUpdated = true;
}
m_pBuff[i] -= 32;
}
}
}