C++之标准库中string的底层实现方式

news2024/11/19 9:28:50

目录

1、Eager Copy(深拷贝)

2、COW(Copy-On-Write)写时复制

2.1写时复制的实现

3、SSO(Short String Optimization)短字符串优化

4、最佳策略

5、线程安全性


我们都知道, std::string的一些基本功能和用法了,但它底层到底是如何实现的呢? 其实在std::string的历史中,出现过几种不同的方式。下面我们来一一揭晓。
我们可以从一个简单的问题来探索,一个std::string对象占据的内存空间有多大,即sizeof(std::string)的值为多大?如果我们在不同的编译器(VC++, GNU, Clang++)上去测试,可能会发现其值并不相同;即使是GNU,不同的版本,获取的值也是不同的。

虽然历史上的实现有多种,但基本上有三种方式:
Eager Copy(深拷贝)
COW(Copy-On-Write 写时复制)
SSO(Short String Optimization-短字符串优化)

每种实现,std::string都包含了下面的信息:
1.字符串的大小
2.能够容纳的字符数量
3.字符串内容本身

1、Eager Copy(深拷贝)

最简单的就是深拷贝了。无论什么情况,都是采用拷贝字符串内容的方式解决,这也是我们之前已经实现过的方式。这种实现方式,在需要对字符串进行频繁复制而又并不改变字符串内容时,效率比较低下。所以需要对其实现进行优化,之后便出现了下面的COW的实现方式。

class String
{
 public:
  String(const String &rhs)
 : _pstr(new char[strlen(rhs._pstr) + 1]())
 {
    strcpy(_pstr, rhs._pstr);
 }
private:
  char *_pstr;
};

2、COW(Copy-On-Write)写时复制

当两个std::string发生复制构造或者赋值时,不会复制字符串内容,而是增加一个引用计数,然后字符串指针进行浅拷贝,其执行效率为O(1)。只有当需要修改其中一个字符串内容时,才执行真正的复制。其实现的示意图,有下面形式:

为了实现的简单,在GNU4.8.4的中,采用的是这种形式。从上面的实现,我们看到引用计数并没有与std::string的数据成员放在一起,为什么呢?大家可以思考一下。
当执行复制构造或赋值时,引用计数加1,std::string对象共享字符串内容;当std::string对象销毁时,并不直接释放字符串所在的空间,而是先将引用计数减1,直到引用计数为0时,则真正释放字符串内容所在的空间。根据这个思路,大家可以自己动手实现一下。
大家再思考一下,既然涉及到了引用计数,那么在多线程环境下,涉及到修改引用计数的操作,是否是线程安全的呢?为了解决这个问题,GNU4.8.4的实现中,采用了原子操作。

总结:当只是进行读操作的时候就进行浅拷贝,然后如果需要进行写操作的时候,再进行深拷贝。实现方式使用浅拷贝加上引用计数。

2.1写时复制的实现

代码:

#include <string.h>
#include <iostream>

using std::cout;
using std::endl;

class String
{
public:
	String()
		: _pstr(new char[5]() + 4)
	{
		cout << "String()" << endl;
		initRefCount();
	}

	String(const char *pstr)
		: _pstr(new char[strlen(pstr) + 5]() + 4)
	{
		cout << "String(const char *)" << endl;
		strcpy(_pstr, pstr);
		initRefCount();
	}

	//String s2 = s1;
	String(const String &rhs)
		: _pstr(rhs._pstr)
	{
		cout << "String(const String &)" << endl;
		increseRefCount();
	}

	//s3 = s1;
	String &operator=(const String &rhs)
	{
		cout << "String &operator=(const String &)" << endl;
		//1、自复制
		if (this != &rhs)
		{
			//2、释放左操作数
			release();

			//3、浅拷贝
			_pstr = rhs._pstr;
			increseRefCount();
		}

		//4、返回*this
		return *this;
	}

private:
	//s3[0] = 'H'
	class CharProxy
	{
	public:
		CharProxy(String &self, size_t idx)
			: _self(self)
			, _idx(idx)
		{

		}
		//写操作
		char &operator=(const char &ch);
		//读操作
		/* friend std::ostream &operator<<(std::ostream &os, const CharProxy &rhs); */
		operator char()//利用由自定义类型向其他类型转换的思想
		{
			cout << "operator char()" << endl;
			return _self._pstr[_idx];
		}
	private:
		String &_self;
		size_t _idx;

	};

public:
	//代理模式
	CharProxy operator[](size_t idx)
	{
		return CharProxy(*this, idx);//方括号运算符执行构造函数
	}

#if 0
	//s3 = s1;
	//s3[0] = 'H'
	char &operator[](size_t idx)
	{
		if (idx < size())
		{
			if (getRefCount() > 1)//共享的
			{
				//深拷贝
				char *tmp = new char[size() + 5]() + 4;
				strcpy(tmp, _pstr);
				//引用计数--
				descreRefCount();

				//浅拷贝
				_pstr = tmp;
				//初始化引用计数
				initRefCount();
			}
			return _pstr[idx];
		}
		else
		{
			static char charNull = '\0';
			return charNull;
		}
	}
#endif

	~String()
	{
		cout << "~String()" << endl;
		release();
	}

	//获取引用计数
	int getRefCount() const
	{
		return *(int *)(_pstr - 4);
	}

	//获取底层的指针
	const char *c_str() const
	{
		return _pstr;
	}

private:
	size_t size() const//字符串的长度
	{
		return strlen(_pstr);
	}

	void initRefCount()//初始化引用技术
	{
		*(int *)(_pstr - 4) = 1;
	}

	void increseRefCount()//增加引用计数
	{
		++*(int *)(_pstr - 4);
	}

	void descreRefCount()//减少引用计数
	{
		--*(int *)(_pstr - 4);
	}

	//释放
	void release()
	{
		descreRefCount();
		if (0 == getRefCount())
		{
			delete[](_pstr - 4);
		}
	}

	friend std::ostream &operator<<(std::ostream &os, const String &rhs);
	//本身是CharProxy中的友元
	/* friend std::ostream &operator<<(std::ostream &os, const String::CharProxy &rhs); */
private:
	char *_pstr;
};

std::ostream &operator<<(std::ostream &os, const String &rhs)
{
	if (rhs._pstr)
	{
		os << rhs._pstr;
	}
	return os;
}

//写操作
//CharProxy = 'H'
char &String::CharProxy::operator=(const char &ch)//注意这一句的书写
{
	if (_idx < _self.size())
	{
		if (_self.getRefCount() > 1)//共享的
		{
			//深拷贝
			char *tmp = new char[_self.size() + 5]() + 4;
			strcpy(tmp, _self._pstr);
			//引用计数--
			_self.descreRefCount();

			//浅拷贝
			_self._pstr = tmp;
			//初始化引用计数
			_self.initRefCount();
		}
		_self._pstr[_idx] = ch;//真正的进行写操作
		return _self._pstr[_idx];
	}
	else
	{
		static char charNull = '\0';
		return charNull;
	}
}

#if 0
std::ostream &operator<<(std::ostream &os, const String::CharProxy &rhs)
{
	os << rhs._self._pstr[rhs._idx];

	return os;
}
#endif

void test()
{
	String s1("hello");
	cout << "s1 = " << s1 << endl;
	cout << "s1.getRefCount() = " << s1.getRefCount() << endl;
	printf("s1'address = %p\n", s1.c_str());

	cout << endl << endl;
	String s2 = s1;
	cout << "s1 = " << s1 << endl;
	cout << "s2 = " << s2 << endl;
	cout << "s1.getRefCount() = " << s1.getRefCount() << endl;
	cout << "s2.getRefCount() = " << s2.getRefCount() << endl;
	printf("s1'address = %p\n", s1.c_str());
	printf("s2'address = %p\n", s2.c_str());

	cout << endl << endl;
	String s3("world");
	cout << "s3 = " << s3 << endl;
	cout << "s3.getRefCount() = " << s3.getRefCount() << endl;
	printf("s3'address = %p\n", s3.c_str());

	cout << endl << endl;
	s3 = s1;
	cout << "s1 = " << s1 << endl;
	cout << "s2 = " << s2 << endl;
	cout << "s3 = " << s3 << endl;
	cout << "s1.getRefCount() = " << s1.getRefCount() << endl;
	cout << "s2.getRefCount() = " << s2.getRefCount() << endl;
	cout << "s3.getRefCount() = " << s3.getRefCount() << endl;
	printf("s1'address = %p\n", s1.c_str());
	printf("s2'address = %p\n", s2.c_str());
	printf("s3'address = %p\n", s3.c_str());

	cout << endl << "对s3[0]执行写操作" << endl;
	//s3.operator[](idx)
	//CharProxy = char
	s3[0] = 'H';//char = char   char ===>CharProxy
	cout << "s1 = " << s1 << endl;
	cout << "s2 = " << s2 << endl;
	cout << "s3 = " << s3 << endl;
	cout << "s1.getRefCount() = " << s1.getRefCount() << endl;
	cout << "s2.getRefCount() = " << s2.getRefCount() << endl;
	cout << "s3.getRefCount() = " << s3.getRefCount() << endl;
	printf("s1'address = %p\n", s1.c_str());
	printf("s2'address = %p\n", s2.c_str());
	printf("s3'address = %p\n", s3.c_str());

	cout << endl << "对s1[0]执行读操作" << endl;
	//cout << CharProxy
	//输出单个字符的时候会进行类型的强转,执行operator char()函数
	cout << "s1[0] = " << s1[0] << endl;//cout << CharProxy===>char
	cout << "s1 = " << s1 << endl;
	cout << "s2 = " << s2 << endl;
	cout << "s3 = " << s3 << endl;
	cout << "s1.getRefCount() = " << s1.getRefCount() << endl;
	cout << "s2.getRefCount() = " << s2.getRefCount() << endl;
	cout << "s3.getRefCount() = " << s3.getRefCount() << endl;
	printf("s1'address = %p\n", s1.c_str());
	printf("s2'address = %p\n", s2.c_str());
	printf("s3'address = %p\n", s3.c_str());
}

int main(int argc, char **argv)
{
	test();
	return 0;
}

运行结果:

String(const char *)
s1 = hello
s1.getRefCount() = 1
s1'address = 00F71134


String(const String &)
s1 = hello
s2 = hello
s1.getRefCount() = 2
s2.getRefCount() = 2
s1'address = 00F71134
s2'address = 00F71134


String(const char *)
s3 = world
s3.getRefCount() = 1
s3'address = 00F7155C


String &operator=(const String &)
s1 = hello
s2 = hello
s3 = hello
s1.getRefCount() = 3
s2.getRefCount() = 3
s3.getRefCount() = 3
s1'address = 00F71134
s2'address = 00F71134
s3'address = 00F71134

对s3[0]执行写操作
s1 = hello
s2 = hello
s3 = Hello
s1.getRefCount() = 2
s2.getRefCount() = 2
s3.getRefCount() = 1
s1'address = 00F71134
s2'address = 00F71134
s3'address = 00F70ECC

对s1[0]执行读操作
operator char()
s1[0] = h
s1 = hello
s2 = hello
s3 = Hello
s1.getRefCount() = 2
s2.getRefCount() = 2
s3.getRefCount() = 1
s1'address = 00F71134
s2'address = 00F71134
s3'address = 00F70ECC
~String()
~String()
~String()

F:\Re-exam test\C study\2024-1-17\Debug\2024-1-17.exe (进程 12224)已退出,返回代码为: 0。
按任意键关闭此窗口...

3、SSO(Short String Optimization)短字符串优化

目前,在VC++、GNU5.x.x以上、Clang++上,std::string实现均采用了SSO的实现。
通常来说,一个程序里用到的字符串大部分都很短小,而在64位机器上,一个char*指针就占用了8个字节,所以SSO就出现了,其核心思想是:发生拷贝时要复制一个指针,对小字符串来说,为啥不直接复制整个字符串呢,说不定还没有复制一个指针的代价大。其实现示意图如下:

当字符串的长度小于等于15个字节时,buffer直接存放整个字符串;当字符串大于15个字节时,buffer存放的就是一个指针,指向堆空间的区域。这样做的好处是,当字符串较小时,直接拷贝字符串,放在string内部,不用获取堆空间,开销小。

总结:当字符串的长度小于16字节的时候,存放在栈上;当字符串的长度大于16字节的时候,就放在堆上

4、最佳策略

以上三种方式,都不能解决所有可能遇到的字符串的情况,各有所长,又各有缺陷。综合考虑所有情况之后,facebook开源的folly库中,实现了一个fbstring, 它根据字符串的不同长度使用不同的拷贝策略,最终每个fbstring对象占据的空间大小都是24字节。
1. 很短的(0~22)字符串用SSO,23字节表示字符串(包括'\0'),1字节表示长度
2. 中等长度的(23~255)字符串用eager copy,8字节字符串指针,8字节size,8字节capacity.
3. 很长的(大于255)字符串用COW, 8字节指针(字符串和引用计数),8字节size,8字节capacity.

5、线程安全性

两个线程同时对同一个字符串进行操作的话, 是不可能线程安全的, 出于性能考虑, C++并没有为string实现线程安全, 毕竟不是所有程序都要用到多线程。
但是两个线程同时对独立的两个string操作时, 必须是安全的. COW技术实现这一点是通过原子的对引用计数进行+1或-1操作。

CPU的原子操作虽然比mutex锁好多了, 但是仍然会带来性能损失, 原因如下:
1.阻止了CPU的乱性执行.
2.两个CPU对同一个地址进行原子操作, 会导致cache失效, 从而重新从内存中读数据.
3.系统通常会lock住比目标地址更大的一片区域,影响逻辑上不相关的地址访问
这也是在多核时代,各大编译器厂商都选择了SS0实现的原因。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1478748.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于SSM SpringBoot vue物流配送人员管理系统

基于SSM SpringBoot vue物流配送人员管理系统 系统功能 登录注册 个人中心 员工管理 考勤信息管理 小区信息管理 打卡信息管理 出勤统计管理 派单信息管理 工资结算管理 任务统计管理 开发环境和技术 开发语言&#xff1a;Java 使用框架: SSM(Spring SpringMVC Mybaits)或…

黑马c++ STL部分 笔记(3) deque容器

双端数组&#xff0c;可以对头端进行插入删除操作 deque与vector区别&#xff1a; vector对于头部的插入删除效率低&#xff0c;数据量越大&#xff0c;效率越低&#xff08;每次头插&#xff0c;后面的元素就往后移&#xff09; deque相对而言&#xff0c;对头部的插入删除速…

Java Web(十)--jQuery

介绍 官网文档&#xff1a;jQuery 教程 jQuery API 中文文档 | jQuery API 中文在线手册 | jquery api 下载 | jquery api chm 下载地址&#xff1a;https://jquery.com/download/%20jQuery jQuery 是一个快速的&#xff0c;简洁的 javaScrip工具库&#xff0c;使用户能更方…

尝鲜18倍速大模型Groq和世界第二AI Mistral(Le Chat)

01 尝鲜 中午,一边吃饭,一边尝试一下最新的AI:Groq,它使用了重新设计的LPU,据说比英伟达的GPU快了18倍。 运行了开源的Mixtral-8x7b模型,屏幕上的文字回复几乎是瞬间的,那种速度感,让人心跳加速。 接着,我尝试了来自欧洲的新贵——Mistral AI的Le Chat。 这个三天前…

前端同时传递文件数据+非文件数据,前后端解决方案

之前录制视频《文件上传组件》的时候有位观众提了个问题&#xff0c;如果我没有理解错的话&#xff0c;应该就是前后同时传递文件数据 非文件数据&#xff0c;前后端数据该如何接收&#xff0c;这里我给出我自己的解决方案 tip:下文在编写前端代码的时候&#xff0c;用到了这篇…

基于SpringBoot的民宿租赁管理系统

文章目录 项目介绍主要功能截图&#xff1a;部分代码展示设计总结项目获取方式 &#x1f345; 作者主页&#xff1a;超级无敌暴龙战士塔塔开 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、 简历模板、学习资料、面试题库【关注我&#xff0c;都给你】 &…

生成voc格式数据集

数据集存放格式&#xff1a;&#xff08;Annotations文件夹放标注的xml文件&#xff0c;JPEGImages文件夹放标注的图片&#xff09; 运行代码&#xff1a; import os import random import xml.etree.ElementTree as ETimport numpy as npdef get_classes(classes_path):with …

nacos开启鉴权+springboot配置用户名密码

nacos默认没有开启鉴权&#xff0c;springboot无需用户名密码即可连接nacos。从2.2.2版本开始&#xff0c;默认控制台也无需登录直接可进行操作。 因此本文记录一下如何开启鉴权&#xff0c;基于nacos2.3.0版本。 编辑nacos服务端的application.properties&#xff1a; # 开…

期货开户保证金保障市场正常运转

期货保证金是什么&#xff1f;在期货市场上&#xff0c;采取保证金交易制度&#xff0c;投资者只需按期货合约的价值&#xff0c;交一定比率少量资金即可参与期货合约买卖交易&#xff0c;这种资金就是期货保证金。期货保证金&#xff08;以下简称保证金〕按性质与作用的不同。…

力扣-移除元素

问题 给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值等于 val 的元素&#xff0c;并返回移除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须仅使用 O(1) 额外空间并 原地 修改输入数组。 元素的顺序可以改变。你不需要考虑数组中超出新长…

手机如何使用NFC卡模拟门禁刷卡

部分手机具备NFC卡刷卡功能&#xff0c;理论上也可模拟门禁卡。 一个功能强大且免费的NFC卡模拟器&#xff0c;可模拟各类门禁卡、电梯卡、部分公司&#xff08;工厂&#xff09;工卡或饭卡、部分学校饭卡、部分图书馆借书卡等各类IC卡&#xff0c;用手机替代卡片去刷门禁、刷…

377组合总和 Ⅳ

题目 给你一个由 不同 整数组成的数组 nums &#xff0c;和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。题目数据保证答案符合 32 位整数范围。示例 1&#xff1a;输入&#xff1a;nums [1,2,3], target 4 输出&#xff1a;7 解释&…

DDD设计学习

之前在研究生项目中遇到的问题便是&#xff1a; 随着业务需求的不断改变&#xff0c;需要在原有项目代码中不断进行修改&#xff0c;导致代码不断累积。 那如何构建高质量应用&#xff0c;那就要遵循三大设计原则&#xff1a; 1.单一职责原则&#xff1a;一个类只负责单一的职…

Unity 佳能SDK 及数据获取

1. 填写信息跟官方申请SDK,大概1-2个工作日会邮件回复你 佳能(中国)- 佳定制(佳能影像产品),SDK,EDSDK,CCAPI,软件开发包下载 2. 将SDK这两个文件放到 Unity Plugins文件夹 3. 把CameraControl 下面只要是绿色的 .cs 文件都复制到Unity 中

windows U盘不能识别

windows U盘不能识别 1、问题描述2、问题分析解决3、把U盘插到windows电脑上试试能不能识别 1、问题描述 windwos u盘不能识别 u盘被拿到mac电脑上做了启动盘之后&#xff0c;就不能被windows识别了。题主很奇怪里面被mac电脑的同学放了什么&#xff0c;因此想到把优盘挂载到L…

Cesium-广告牌

创作来源 1、道路标识牌 2、视频广告 创作思路 1、创建有颜色柱体 2、创建长方体并带有纹理 3、将视频问题贴到长方体上 实现步骤 1、创建柱体 /*** 获取柱状几何对象* param radius* param height* return {Geometry}*/ export const getCylinderGeometry (radius, he…

how to deploy parent pom without module deployment

how to deploy parent pom without module deployment deploy -N -f pom.xml

从1-20之间随机抽取5个数,输出抽取出来的5个数之和与剩余未被抽取的15个数之和

从1-20之间随机抽取5个数&#xff0c;输出抽取出来的5个数之和与剩余未被抽取的15个数之和&#xff0c;每个答案独占一行 代码&#xff1a; #include <cstdio> #include <stdlib.h> int main() {int arr[20] { 0 };printf("抽取的5个随机数是&#xff1a;&…

Unity(第八部)Vector3的三维向量和旋转(坐标和缩放也简单讲了一下)

对了&#xff0c;Unity的生命周期自行百度吧&#xff1b;我这边整理的都不是很满意 Vector 是结构体 Vector2是指里面有两个变量 Vector3是指里面有三个变量 Vector4是指里面有四个变量 Vector3常用的变量就是x y z,所以&#xff0c;它可以代表坐标、旋转、缩放、三维向量 创…

Vue3 学习笔记(Day5)

「写在前面」 本文为尚硅谷禹神 Vue3 教程的学习笔记。本着自己学习、分享他人的态度&#xff0c;分享学习笔记&#xff0c;希望能对大家有所帮助。推荐先按顺序阅读往期内容&#xff1a; 1. Vue3 学习笔记&#xff08;Day1&#xff09; 2. Vue3 学习笔记&#xff08;Day2&…