在模拟String类时,我们既有深拷贝的方法,又有写时拷贝的方法,这篇博客就是好好总结一下写时拷贝技术
什么是写时拷贝呢?
写时拷贝技术(Copy On Write)是一个被使用在程序设计领域的最佳化策略,其基础的观念是,如果有多个呼叫者(callers)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently)。
此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy)被建立。
以上一段话来自百度百科~~
通俗的讲,模拟实现String类时,我们在赋值时使用浅拷贝,然后当某个对象的字符串要改变时再对字符串进行拷贝复制,然后再进行修改,这就是写时拷贝
然而我们如何得知有多少个对象指向同一个内存空间呢?
这里用到的方法就是写时拷贝
注意,这个_pCount不能是普通变量,必须是当前指向的类共用一个
能不能使用静态变量呢?答案是不能,因为静态变量是所有的类的对象成员共用一个
所以就用指针,在开辟字符串内存空间的时候,给_pCount也开辟一段内存空间
//引用计数写实拷贝
class String {
public:
//成员函数
private:
//成员变量有一个字符串和一个用来计数的
char* _str;
size_t* _pCount;
};
使用引用计数实现写时拷贝有两种方法
第一种是给_pCount正常开辟四个字节的内存空间,如上图,第一种的代码如下:
//引用计数写实拷贝
class String {
public:
//构造函数
String(char* str);
//拷贝构造函数
String(const String& str);
//等号赋值操作符重载
String& operator=(const String& s);
//返回字符串
const char* C_str();
//写时拷贝
void CopyOnWrite();
char& operator[](size_t pos);
//其他的一些操作和深拷贝一样,只是需要CopyOnWrite()来复制一下
//析构函数
~String();
private:
//成员变量有一个字符串和一个用来计数的
char* _str;
size_t* _pCount;
};
接下来具体看一下如何实现
1.构造函数
//构造函数
String::String(char* str = "")
:_str(new char[strlen(str) + 1])
, _pCount(new size_t(1)) {
strcpy(_str, str);
}
给_pCount开辟字节大小为sizeof(size_t)大小的内存空间,(size_t在64位下类型为long unsigned int)
2.拷贝构造函数
//拷贝构造函数
String::String(const String& s)
:_str(s._str)
,_pCount(s._pCount){
//把引用计数增加个1
(*_pCount)++;
}
这里的拷贝构造就是使用的浅拷贝,只是对成员变量进行简单的赋值
3.赋值操作符重载
//等号赋值操作符重载
String& String::operator=(const String& s) {
if (&s != this) {
if (--(*_pCount) == 0) {
//调用一个析构函数
String::~String();
}
_str = s._str;
_pCount = s._pCount;
(*_pCount)++;
}
return *this;
}
赋值操作符重载,我们需要先判断引用计数是不是为1,表明只有当前对象指向,那我们就析构它,否则就只需要让引用计数减1,然后让_str指向赋值的那个对象
当当前对象引用计数为1时:
当前对象引用计数不为1时:
4.析构函数
//析构函数
String::~String() {
if (*_pCount == 1) {
delete[] _str;
_str = NULL;
delete _pCount;
_pCount = NULL;
}
else {
(*_pCount)--;
}
cout << "~String" << endl;
}
析构当前对象时,需要判断是不是需要释放内存空间,如果只有_pCount为1时,表明只有当前对象这段内存空间,就可以直接析构,否则就只是让引用计数自减
5.CopyOnWrite
//写时拷贝
void String::CopyOnWrite() {
//如果引用计数等于1,就表示当前字符串只有一个指针指向
//那么无论进行任何操作都可以,为所欲为
if (*_pCount > 1) {
//把旧的引用计数减1
--(*_pCount);
char* new_str = new char[strlen(_str) + 1];
strcpy(new_str, _str);
_str = new_str;
//给新拷贝成功的_str的计数开辟一段内存空间
_pCount = new size_t(1);
}
}
如图,如果引用计数大于1的话,就给字符串和_pCount动态的申请内存空间
写时拷贝基础的几个函数就是上面这些了,剩下的增删改查无非就是在函数中先调用CopyOnWrite()函数即可,下面以[]操作符重载为例
6.[]操作符重载
char& String::operator[](size_t pos) {
CopyOnWrite();
return _str[pos];
}
由于[]操作符可能会更改_str中的某个字符,所以就需要进行写时拷贝
前面我们也说了,写时拷贝有两种方法,上面的方法是第一种,下面介绍第二种方法,把_pCount放在_str内存空间的开头
这个方法类似于“new 类型[size_t size]”这种方法,在申请的空间开头多开辟四个四节来存储,如图
当然,这里是不需要_pCount这个变量的,我这里写上只是为了方便观看,类的头文件如下:
class String {
public:
//构造函数
String(char* str);
int& GetCount();
//拷贝构造函数
String(const String& s);
//赋值运算符重载
String& operator=(const String& s);
const char* c_str();
void CopyOnWrite();
char& operator[](size_t pos);
//析构函数
~String();
void Show() {
cout <<"_str = "<< _str << endl;
cout << "_pCount = " << GetCount() << endl;
}
private:
char* _str;//引用计数在头上
};
注意,这里有一个小细节,类中很多成员函数都是内联函数,所以在类外面定义的时候,最好加上内联~(我懒得加)
1.构造函数
//构造函数
String::String(char* str = "")
:_str(new char[strlen(str) + 1 + 4]) {
//多申请四个字节存储_pCount
_str += 4;
strcpy(_str, str);
GetCount() = 1;
}
给_str多开辟四个字节,然后让那四个字节存储引用计数,为了方便,把取得引用计数的方法封装成函数GerCount,函数代码如下:
int& String::GetCount() {
return *(int*)(_str - 4);
}
2.拷贝构造函数
拷贝构造函数很简单,只是简单的赋值(浅拷贝),和编译器自己实现的拷贝构造函数是一样的,但是这里还是需要对引用计数加1
//拷贝构造函数
String::String(const String& s)
:_str(s._str){
GetCount()++;
}
3.赋值运算符重载
//赋值运算符重载
String& String::operator=(const String& s) {
if (&s != this) {
if (GetCount() == 1) {
//只有当前一个引用,可以直接析构
String::~String();
}
else {
--GetCount();
}
_str = s._str;
++GetCount();
}
return *this;
}
和第一种方法的思路一样,都是先判断旧的_str是否需要删除,如果需要就直接调用析构函数析构,如果不需要就让引用计数自减,然后再进行一波浅拷贝
4.写时拷贝
//写实拷贝
void String::CopyOnWrite() {
//如果引用计数大于1,即有不止一个引用,那么就需要重新拷贝了
//因为能用到这个函数的地方,基本都是需要重新拷贝一份了
if (GetCount() > 1) {
//原有的引用计数减1
--GetCount();
//先申请一段足够的内存空间
char* new_str = new char[strlen(_str) + 5];
new_str += 4;
//然后把字符串赋值过来
strcpy(new_str, _str);
//再新申请的内存空间的引用计数初始化为1
*(int*)(new_str) = 1;
//最后再让_str指向新拷贝的内存空间
_str = new_str;
}
}
写时拷贝的思路和第一种方法很相似,只是具体代码实现呢上有点小差异
模拟实现String类中的引用计数就总结到这里,下面是完整代码的链接
写时拷贝方法一
写时拷贝方法二