C++(9)——引用计数实现写时拷贝(包含String类的实现)

2023-11-13

在之前的学习中,
我们谈到了字符串的深拷贝与浅拷贝,在浅拷贝中,由于多个对象共用同一块内存空间,导致同一块空间被释放多次而出现问题,那能否保证:当多个对象共享一块空间时,该空间最终只释放一次呢?

这就是我们接下来要谈的问题:
使用浅拷贝不浪费内存空间却容易发生内存泄漏问题,使用深拷贝不会发生内存泄漏但是会不断开辟新的空间,内存利用率低。因此结合深浅拷贝的特点产生了写时拷贝。

一、什么是写时拷贝?

当你在读取一片空间时,系统并不会为你开辟一个一模一样的空间给你;只有在当你真正修改的时候,才会开辟一片空间给你。

二、怎么实现写时拷贝呢?

  1. 使用引用计数来实现。所以我们在分配空间时需要多分配4个字节,来记录有多少个指针指向这个空间。
  2. 有新的指针指向这篇空间时,那么引用计数就加一;当有一个指针要释放该空间时,那么引用计数就减一。
  3. 当有指针要修改这片空间时,则为该指针重新分配自己的空间,原空间的引用计数减一,新空间的引用计数加一。

自己实现的String类

引用计数

对于我们之前所写的String类,在拷贝构造时,深拷贝方式使得使得各个对象都指向了独立的堆区空间,造成了内存和资源的浪费。因此,我们引入引用计数:
其原理如下:原理:,当多个对象共享一块资源时,要保证该资源只释放一次, 只需记录有多少个对象在使用该资源即可,每减少(增加)一个对象使用, 给该计数减一(加一),当最后一个对象不使用时,该对象负责将资源释放掉即可 。

class String
{
protected:
	struct StrNode
	{
		int ref;//对象的引用计数
		int len;//字符串长度
		int size;//字符串占据的空间大小
		char data[];//或char data[0]
	};
private:
	StrNode* pstr;
	String(StrNode *p):pstr(p){}
public:
	String(const char* p = NULL) :pstr(NULL)
	{
		if (p != NULL)
		{
			int len = strlen(p);
			pstr = (StrNode*)malloc(sizeof(StrNode) + len * 2 + 1);
			pstr->ref = 1;
			pstr->len = len;
			pstr->size = len * 2;
			strcpy(pstr->data, p);
		}
	}
	String(const String& s) :pstr(NULL)
	{
		if (s.pstr != NULL)
		{
			pstr = s.pstr;
			pstr->ref += 1;
		}
	}
	String & operator=(const String &s)
	{
		if(this == &s)return *this;
		
		if(this->pstr != NULL && --this->pstr->ref == 0) 
		{
			free(this->pstr);
		}

		this->pstr = s.pstr;
		if(this->pstr != NULL)
		{
			this->pstr->ref += 1;
		}
		return *this;
	}
	~String()
	{
		if (pstr != NULL && --pstr->ref == 0)
		{
			free(pstr);
		}
		pstr = NULL;
	}
	char operator[](const int index) const//这里不以引用返回也是为了减少内存访问次数
	{
		if(pstr == NULL) exit(1);
		assert(index >= 0 && index <= pstr->len-1);//hello len-5
		return pstr->data[index];
	}
	void modify(const int index,const char ch)
	{
		if(pstr == NULL) exit(1);
		assert(index >= 0 && index <= pstr->len-1); 

		if(pstr->ref > 1)//写时拷贝
		{
			int total = sizeof(StrNode) + pstr->size + 1;
			StrNode *newnode = (StrNode *)malloc(total);
			memmove(newnode,pstr->data,total);
			newnode->ref = 1;
			pstr->ref -= 1;
			pstr = newnode;
		} 

		pstr->data[index] = ch;
	}
	// const char & operator[](const int index) const
	// { 
	// 	//return (*this)[index];
	// 	//此时不能够成功调用,因为此方法是常方法
	// 	return (*const_cast<String*>(this))[index];//去除this指针的常性,这样写可读性较强
	// 	//等同于return (*(String*)this)[index]
	// }
	 
	String(String &&s):pstr(NULL)
	{
		pstr = s.pstr;
		s.pstr = NULL;
	}
	String &operator=(String &&s)
	{
		if(this == &s)
		{
			return *this;
		}
		if(pstr != NULL && --pstr->ref == 0)
		{
			free(pstr);
		}
		pstr = s.pstr;
		s.pstr = NULL;
		return *this;
	}
	String operator+(const String &s)const
	{
		if (pstr == NULL && s.pstr == NULL)
		 {
			 return String();
		 }
		 else if (pstr != NULL && s.pstr == NULL)
		 {
			 return *this;
		 }
		 else if (pstr == NULL && s.pstr != NULL)
		 {
			 return s;
		 }
		 else
		 {
			 int total = (pstr->len + s.pstr->len)*2;
			 StrNode* newsp = (StrNode*)malloc(sizeof(StrNode) + total + 1);
			 strcpy(newsp->data, pstr->data);
			 strcat(newsp->data, s.pstr->data);
			 newsp->ref = 1;
			 newsp->len = pstr->len + s.pstr->len ;
			 newsp->size = total;
			 return String(newsp);
		 }
	}
	String & operator+=(const String & s)
	{
		if(this->pstr != NULL && s.pstr != NULL)
		{
			if(this->pstr->ref > 1)//此时考虑建立一个副本,然后重新+=,原来的字符串引用计数减一
			{
				//同时建立副本时还需要考虑加进来的字符串的大小,空间一定要足够
				int total = pstr->len + s.pstr->len;
				this->pstr->ref -= 1;
				char *tmp = this->pstr->data;
				this->pstr = (StrNode*)malloc(sizeof(StrNode)+total*2);
				strcpy(this->pstr->data,tmp);
				strcat(this->pstr->data,s.pstr->data);
				this->pstr->ref = 1;
				this->pstr->len = total;
				this->pstr->size = total * 2;
			}
		}
		else if (this->pstr == NULL && s.pstr != NULL)
		 {
			 this->pstr = s.pstr;
			 this->pstr->ref += 1;
		 }
		//只有一个对象独占时,空间不足时扩容,否则无需对空间大小进行操作
		else 
		{
			int total = this->pstr->len + s.pstr->len;
			//空间不足
			if(this->pstr->size < total)
			{
				this->pstr = (StrNode*)realloc(this->pstr,sizeof(StrNode)+total*2+1);
				this->pstr->size = total * 2;
			}
				strcat(this->pstr->data,s.pstr->data);//此处s1 += s1时会引发异常
				this->pstr->len = total;
		}
		return *this;
	}
	ostream & operator<<(ostream & out) const
	{
		if(pstr != NULL)
		{
			out<<pstr->data;
		}
		return out;
	}
};
ostream & operator<<(ostream & out,const String &s)
{
	s<<out;
	return out;
}

这里的data[]是一个柔性数组,也是C99、C11标准中给出的新的设计方法,数组的大小声明为0,或者不给出大小,称之为柔性数组,**注意,全局数组和局部数组不能这样定义,**这种定义只在结构体或者类中可以定义。
在这里插入图片描述
比如一个struct node{int a;int b;char data[];}
在主函数申请空间时,我们可以这样写struct node *sp = (struct node *)malloc(sizeof(struct node) + 50),一共58个字节的空间,前八个为a,sizeof计算的结果就是8,因为data作为标识符不占据空间,后面50个字节动态分配给了data。

在如下的代码块中,内存分布是这样的:
String s1(“hello”);
String s2(s1);
String s3(s1);
String s4(s1);
在这里插入图片描述

之后,我们再来写赋值语句,需要考虑如下的几个问题:

  • 自赋值判断
  • 当前字符串无人引用是要自动销毁
String & operator=(const String &s)
	{
		if(this == &s)return *this;
		
		if(this->pstr != NULL && --this->pstr->ref == 0) 
		{
			free(this->pstr);
		}

		this->pstr = s.pstr;
		if(this->pstr != NULL)
		{
			this->pstr->ref += 1;
		}
		return *this;
	}

下一个问题:在修改字符串的内容时,如果有多个对象指向同一个字符串,那么在修改字符串的某一位时,另外的对象也要修改,这里考虑到共享性的问题,所以需要写时拷贝

char & operator[](const int index) 
	{
		if(pstr == NULL) exit(1);
		assert(index > 0 && index <= pstr->len-1);//hello
												//01234  len-5
		if(pstr->ref > 1)
		{
			int total = sizeof(StrNode) + pstr->size + 1;
			StrNode *newnode = (StrNode *)malloc(total);
			memmove(newnode,pstr->data,total);
			newnode->ref = 1;
			pstr->ref -= 1;
			pstr = newnode;
		}
		return pstr->data[index];
	}
	const char & operator[](const int index) const
	{
		return (*this)[index];
	}
int main()
{
	String s1("hello");
	String(s1);
	s1[0] = 'H';
	cout<<s2<<endl;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++(9)——引用计数实现写时拷贝(包含String类的实现) 的相关文章

随机推荐

  • 【GD32F427开发板试用】 CAN总线收发测试

    本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动 更多开发板试用活动请关注极术社区网站 作者 meijing 这篇测试下CAN通信的收发测试 代码使用库例程中修改 硬件部分 测试用到了CAN0 串口0和定时器1 1 gt
  • 【读书笔记】Linux高性能服务器编程(第二篇 第五章)

    第五章 Linux网络编程基础API 5 1 socket地址API 5 1 1 主机字节序和网络字节序 字节序分为 1 大端字节序 一个整数的高位字节 23 31 bit 存储在内存的低地址处 低位字节 0 7 bit 存储在内存的高地址
  • okhttp Retrofit统一处理返回请求 okhttp Retrofit统一处理返回数据

    okhttp Retrofit统一处理返回请求 okhttp Retrofit统一处理返回数据 Gson gson new GsonBuilder serializeNulls create 1 配置创建okhttp客户端 OkHttpCl
  • 《Python核心编程》读书笔记

    第一章 Python对象 1 1 Python对象 Python使用对象模型来存储数据 所有的Python对象都有三个特性 身份 类型和值 身份可以使用内建函数id 得到 类型可以使用内建函数type 查看 1 2 标准类型 数字 整形 I
  • WGS84的理解

    转载 https support virtual surveyor com en support solutions articles 1000261351 what is wgs84 text When 20you 20determine
  • Xilinx AXI VIP使用教程

    Xilinx提供了用于验证AXI相关设计的AXI VIP AXI Verification IP 它可以对自己设计的AXI接口模块进行全方位的验证 如使用VIP的Master Passthrough Slave三种模式对自己写的AXI接口进
  • 宝塔面板登录失败

    这是七月初出现的宝塔面板登录问题 如下 登录界面一直在转圈圈 只是最近两周在写项目和刷算法题 无暇顾及这个问题 现在给出解决方法 2023 07 24午 首先保证网络环境良好 手机热点很不靠谱的 最好是直连路由器 家里的或者学校宿舍的 其实
  • 面板数据实证过程-基于政府补贴对企业研发投入的影响分析

    一 数据来源 国泰安CSMAR 二 数据对象 同花顺BKDC26股票 新能源板块 内所有上市公司 剔除ST及数据缺失的公司 三 年份选取 2016 2019年 四 变量列举 变量表 被解释变量 RDratio 企业研发投入程度 主解释变量
  • 如何调用高德地图api

    首先注册成为高德地图开发者 创建应用后申请key 1 引入高德地图API 2 创建地图容器 div div width 300px height 180px 指定大小样式 3 创建默认地图 方式一 var map new AMap Map
  • 饥荒专用服务器全图显示代码,饥荒地图怎么看 饥荒地图全开代码

    最近很多玩家表示在玩 饥荒 的时候 经常会有遇到各种各样的问题 比如新建一个世界然后辛辛苦苦跑完整张地图 结果发现世界里没猪王 整个地图居然没有六眼飞虫雕像 猪王雕像地洞各种靠的近 结果发现全图没海象 小编今天为各位玩家带来了一些解决问题的
  • mysql的全量备份和增量备份

    文章目录 全量备份 1 创建mysqldump脚本 1 1 找一个目录 这里选择放到 data mysqlbackup下 创建shell脚本 1 2 添加内容 1 3给脚本赋予执行权限 2 crontab 2 1查看cron状态 2 1创建
  • GTX1660TI 算力

    7 5
  • 前端面试题 vue专项,共计58道

    vue篇 1 什么是MVVM 2 Vue声明周期 3 为什么vue中data必须是一个函数 4 vue router有几种导航钩子 5 Vue的v show和v if区别 6 vue loader是什么 使用它的用途有哪些 7 计算属性和w
  • 使用Opencv+SVM+Hog进行行人识别的代码

    OpenCV include
  • CLOUD 云计算进阶(二)- openstack 服务搭建与应用

    什么是云计算 基于互联网的相关服务的增加 使用和交互模式 这种模式提供可用的 便捷的 按需的网络访问进入可配置的计算机网络共享池 这些资源能够被快速提供 只投入很少的管理工作 或者与服务供应商进行很少的交互 通常通过互联网来提供动态易拓展且
  • python绘制散点图和折线图

    散点图 一般和相关分析 回归分析结合使用 import pandas import matplotlib import matplotlib pyplot as plt plot circle pandas read csv D Pytho
  • webUI自动化图片验证码登录、服务器自动登录、复用已打开浏览器登录(动态验证码或扫码登录)

    接上一篇
  • 软件外包开发流程分析

    软件系统的开发过程分为需求分析 设计 开发 测试 部署 上线和运维共七个阶段 每个阶段都有大量的工作要做 随着软件规模越来越大 科学的管理这个流程非常重要 管理不好就可能会导致项目最终失败 今天和大家分享软件开发流程 希望对大家有所帮助 北
  • ROS下使用乐视RGB-D深度相机/Orbbec Astra Pro显示图像和点云

    ROS下使用乐视RGB D深度相机显示图像和点云 1 正常安装 1 1 安装依赖 1 2 建立工作空间 1 3 克隆代码 1 4 Create astra udev rule 1 5 编译源码包 1 6 修改astrapro launch
  • C++(9)——引用计数实现写时拷贝(包含String类的实现)

    在之前的学习中 我们谈到了字符串的深拷贝与浅拷贝 在浅拷贝中 由于多个对象共用同一块内存空间 导致同一块空间被释放多次而出现问题 那能否保证 当多个对象共享一块空间时 该空间最终只释放一次呢 这就是我们接下来要谈的问题 使用浅拷贝不浪费内存