【C++基础】模拟实现String类的写时拷贝

在模拟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类中的引用计数就总结到这里,下面是完整代码的链接

写时拷贝方法一

写时拷贝方法二


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值