c++实现string的写时复制技术(COW)——注释超细版
COW(copy-on-write)
前言
当字符串进行复制的时候,如很长的字符串(2k大小),如果全部采用堆空间存储的话那是非常浪费空间的,复制一次变成4k大小,两次6k…
所以为了节约空间,在两个字符串都是相同内容的时候,将复制后的指针指向原始字符串的地址空间,然后采用引用计数的方式对该空间的引用数+1;当修改某一指针的内容时,其实就是改变了字符串数据,那么显然变成了一个新的字符串,这时候就需要给这个新的字符串重新申请一块堆空间了,然后对原始字符串的引用计数-1。
代码实现
#include <iostream>
#include <string.h>
using namespace std;
class String
{
private:
char *_pstr;
public:
String()
// 申请5个字节的堆空间(4个字节的int类型的引用计数,一个字节字符串'\0'
// 向后偏移四个字节的空间(越过引用计数直接到字符串)
: _pstr(new char[5]() + 4)
{
// static_cast不能用于两个明确类型的指针进行转换,可以使用reinterpret_cast转换
// *static_cast<int *>(_pstr - 4) = 1; // error
// *reinterpret_cast<int *>(_pstr - 4) = 1; // ok, 但是不安全
*(int *)(_pstr - 4) = 1; // 指针偏移到引用计数位,初始值为1
cout << "String()" << endl;
}
// 有参构造函数
String(const char *pstr)
: _pstr(new char[strlen(pstr) + 5]() + 4)
{
strcpy(_pstr, pstr);
*(int *)(_pstr - 4) = 1;
cout << "String(const char *pstr)" << endl;
}
// 拷贝构造函数,由于内容一样,所以采用浅拷贝,两个指针指向同一个空间,
// 增加引用计数值,不重新申请内存空间
String(const String &rhs)
// 指针指向同一块堆空间
: _pstr(rhs._pstr)
{
++*(int *)(_pstr - 4); // 拷贝构造函数,引用计数+1
cout << "String(const String &rhs)" << endl;
}
// 赋值运算符函数,还是相同的字符串,所以采用浅拷贝
String &operator=(const String &rhs)
{
cout << "String operator=(const String &rhs)" << endl;
// 判断是否是自己赋值给自己
if (this != &rhs)
{
// 如果被赋值的字符串引用计数为1,那么就释放(被赋值的)原始内存空间,
// 防止脏数据(如被赋值的数据长度大于赋值过来的数据)
if (1 == *(int *)(_pstr - 4))
{
delete[](_pstr - 4);
}
// 两个指针指向同一个字符串,实现浅拷贝
_pstr = rhs._pstr;
// 对字符串引用计数+1
++*(int *)(_pstr - 4);
}
// 返回字符串
return *this;
}
// 析构函数
~String()
{
// 当引用计数为1时,表明没有两个指针同时指向一块内存空间,可以直接删除
if (1 == *(int *)(_pstr - 4))
{
delete[](_pstr - 4);
_pstr = nullptr;
}
cout << "~String()" << endl;
}
class charProxy
{
private:
String &_self;
size_t _idx;
public:
// 需要修改数据,所以参数应该有需要修改的字符串、下标
charProxy(String &self, size_t idx)
: _self(self), _idx(idx) {}
// 实现下标写操作
char &operator=(const char &rhs)
{
cout << "char &operator=(const char &rhs)" << endl;
// 判断下标合法性
if (_idx < _self.size() && _idx >= 0)
{
// 如果是共享的,此时需要修改数据了所以需要申请一块新的堆空间
if (_self.refCount() > 1)
{
char *ptmp = new char[_self.size() + 5]() + 4;
strcpy(ptmp, _self._pstr);
// 由于分配了新的空间所以引用计数-1
--*(int *)(_self._pstr - 4);
// 将新的字符串的指针赋值给_pstr,这样才可以传出去
_self._pstr = ptmp;
// 改写后新的字符串的引用计数变为1
*(int *)(_self._pstr - 4) = 1;
}
// 真正赋值操作
_self._pstr[_idx] = rhs;
// 返回修改后的值
return _self._pstr[_idx];
}
else
{
cout << "error idx" << endl;
static char nullchar = '\0';
return nullchar;
}
}
// 隐式转换:将charProxy转换为char类型,免去重载新的输出流运算符
operator char()
{
return _self._pstr[_idx];
}
};
// 重载下标访问运算符,返回类型为charProxy类,为了区分对数据的读操作与写操作,
// 如果写成char &operator[](size_t idx)这种,没有办法区分读下标操作还是写下标操作,
// 如果返回类型为charProxy,那么在进行赋值操作的时候会触发charProxy的赋值运算符函数
// 这样就去分了string的读写操作,单独读下标是不会触发charProxy的赋值运算符函数的
charProxy operator[](size_t idx)
{
return charProxy(*this, idx);
}
// 字符串大小获取函数
size_t size() const
{
return strlen(_pstr);
}
// 引用计数值获取函数
int refCount() const
{
return *(int *)(_pstr - 4);
}
const char *strAddr() const
{
return _pstr;
}
// 重载输出流运算符,输出自定义类型
friend ostream &operator<<(ostream &os, const String &rhs);
};
ostream &operator<<(ostream &os, const String &rhs)
{
if (rhs._pstr)
{
os << rhs._pstr;
}
return os;
}
int main(int argc, char **argv)
{
String s1("hello");
cout << "s1 = " << s1 << endl;
cout << "s1.refcount = " << s1.refCount() << endl;
printf("s1.addr = %p\n", s1.strAddr());
cout << endl;
cout << "拷贝操作String s2 = s1" << endl;
String s2 = s1;
cout << "s1 = " << s1 << endl;
cout << "s2 = " << s2 << endl;
cout << "s1.refcount = " << s1.refCount() << endl;
printf("s1.addr = %p\n", s1.strAddr());
cout << "s2.refcount = " << s2.refCount() << endl;
printf("s2.addr = %p\n", s2.strAddr());
s2.strAddr();
cout << endl;
cout << "赋值操作s3 = s2" << endl;
String s3;
s3 = s2;
cout << "s2.refcount = " << s2.refCount() << endl;
printf("s2.addr = %p\n", s2.strAddr());
cout << "s3.refcount = " << s3.refCount() << endl;
printf("s3.addr = %p\n", s3.strAddr());
cout << endl;
cout << "读下标操作" << endl;
cout << "s2[0] = " << s2[0] << ", s1 = " << s1 << endl;
cout << "s1.refcount = " << s1.refCount() << endl;
printf("s1.addr = %p\n", s1.strAddr());
cout << "s2.refcount = " << s2.refCount() << endl;
printf("s2.addr = %p\n", s2.strAddr());
cout << endl;
cout << "对数据写操作,将会触发写时复制技术!" << endl;
s2[0] = 'H';
cout << "s2[0] = " << s2[0] << ", s2 = " << s2 << endl;
cout << "s1.refcount = " << s1.refCount() << endl;
printf("s1.addr = %p\n", s1.strAddr());
cout << "s2.refcount = " << s2.refCount() << endl;
printf("s2.addr = %p\n", s2.strAddr());
cout << "s3.refcount = " << s3.refCount() << endl;
printf("s3.addr = %p\n", s3.strAddr());
return 0;
}
运行结果
注意看内存地址变化,及引用计数的变化
String(const char *pstr)
s1 = hello
s1.refcount = 1
s1.addr = 0x55d2299c0e74
拷贝操作String s2 = s1
String(const String &rhs)
s1 = hello
s2 = hello
s1.refcount = 2
s1.addr = 0x55d2299c0e74
s2.refcount = 2
s2.addr = 0x55d2299c0e74
赋值操作s3 = s2
String()
String operator=(const String &rhs)
s2.refcount = 3
s2.addr = 0x55d2299c0e74
s3.refcount = 3
s3.addr = 0x55d2299c0e74
读下标操作
s2[0] = h, s1 = hello
s1.refcount = 3
s1.addr = 0x55d2299c0e74
s2.refcount = 3
s2.addr = 0x55d2299c0e74
对数据写操作,将会触发写时复制技术!
char &operator=(const char &rhs)
s2[0] = H, s2 = Hello
s1.refcount = 2
s1.addr = 0x55d2299c0e74
s2.refcount = 1
s2.addr = 0x55d2299c12a4
s3.refcount = 2
s3.addr = 0x55d2299c0e74
~String()
~String()
~String()
如果本文对你有帮助,记得一键三连哦,一键三连笑哈哈,代码能力顶呱呱!
本人能力有限,如有错误,望不吝指正;原创不易,欢迎转载,转载请注明出处!