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

目录

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、返回*thisreturn *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;
}
#endifvoid 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 = chars3[0] = 'H';//char = char   char ===>CharProxycout << "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===>charcout << "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 = 00F71134String(const String &)
s1 = hello
s2 = hello
s1.getRefCount() = 2
s2.getRefCount() = 2
s1'address = 00F71134
s2'address = 00F71134String(const char *)
s3 = world
s3.getRefCount() = 1
s3'address = 00F7155CString &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://xiahunao.cn/news/2815075.html

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

相关文章

李沐动手学习深度学习——3.4练习

理解极大似然估计 很巧妙的解释了为什么使用均方误差&#xff0c;因为均方误差是一种似然估计的变种&#xff0c;而对于逻辑回归softmax而言&#xff0c;更好的解释了其中存在exp 1. 我们可以更深入地探讨指数族与softmax之间的联系。 1. 计算softmax交叉熵损失l(y, ˆy)的二阶…

【C++历练之路】map与set的必备使用指南

W...Y的主页 &#x1f60a; 代码仓库的分享&#x1f495; &#x1f354;序言&#xff1a; C作为一门历史悠久且功能强大的编程语言&#xff0c;其标准模板库&#xff08;STL&#xff09;为开发者提供了一套丰富的数据结构和算法&#xff0c;极大地促进了软件开发的效率和质量…

Python打发无聊时光:10.用flask创造按键控制的网页小游戏

游戏介绍: 《秦蓝大冒险》是一款简单而紧张的追逐游戏。在这个游戏中&#xff0c;玩家将控制一名名叫“吕千”的角色&#xff0c;试图在一个封闭的空间内逃避一个名为“秦蓝”的追逐者。随着时间的推移&#xff0c;“秦蓝”会不断追踪玩家的位置&#xff0c;努力捕捉到他。 游…

干货!Python字符串填充、去除、分割与合并

1.center() 将字符串按照指定内容填充到指定长度&#xff0c;默认填充的内容是空格 str1 "今天天气好晴朗"print(str1.center(50)) # 使用空间将原字符串填充到50个长度&#xff0c;原内容居中print(str1.center(50, "*")) # 使用 * 将原字符串填…

免费原型工具大集合,让你设计更轻松

对于产品经理或UI/UX设计师来说&#xff0c;一个好的原型设计工具是非常重要的。一个好的原型设计软件可以帮助你快速构建一个还原度高、信息架构清晰的原型图&#xff0c;大大降低工作中与同事的沟通成本&#xff0c;更有效地促进工作。 那么&#xff0c;一个易于使用和免费的…

2024年计算机科学与电子通讯工程国际会议(ICCSECE 2024)

2024年计算机科学与电子通讯工程国际会议&#xff08;ICCSECE 2024&#xff09; 重要信息 会议官网&#xff1a;http://www.iccsece.com会议地址&#xff1a;郑州召开日期&#xff1a;2024.03.25截稿日期&#xff1a;2024.03.15 &#xff08;先投稿&#xff0c;先审核&#xff…

【Linux】进程优先级以及Linux内核进程调度队列的简要介绍

进程优先级 基本概念查看系统进程修改进程的优先级Linux2.6内核进程调度队列的简要介绍和进程优先级有关的概念进程切换 基本概念 为什么会存在进程优先级&#xff1f;   进程优先级用于确定在资源竞争的情况下&#xff0c;哪个进程将被操作系统调度为下一个运行的进程。进程…

【C语言】数据存储篇,内存中的数据存储----C语言整型,浮点数的数据在内存中的存储以及大小端字节序【图文详解】

欢迎来CILMY23的博客喔&#xff0c;本篇为​【C语言】数据存储篇&#xff0c;内存中的数据存储----C语言整型&#xff0c;浮点数的数据在内存中的存储以及大小端字节序【图文详解】&#xff0c;感谢观看&#xff0c;支持的可以给个一键三连&#xff0c;点赞关注收藏。 前言 C语…

Mysql索引优化导致死锁问题

1、背景 随着公司业务的发展&#xff0c;商品库存从商品中心独立出来成为一个独立的系统&#xff0c;承接主站商品库存校验、订单库存扣减、售后库存释放等业务。在上线之前我们对于核心接口进行了压测&#xff0c;压测过程中出现了MySQL 5.6.35死锁现象&#xff0c;通过日志发…

vscode——本地配置(C和C++环境配置)(2)

vscode——本地配置&#xff08;2&#xff09; 配置C语言编译看看.json文件编译多个C文件C/C调试 今天我们继续来看vscode的配置&#xff0c;如果没看过上一次的文章&#xff0c;大家可以点击&#xff1a; https://blog.csdn.net/qq_67693066/article/details/136315696 配置C语…

NebulaGraph入门

感谢阅读 官方文档链接NebulaGraph简介nGQLnGQL简介占位标识符和占位符值注释实列大小写区分关键字 基本概念以及相关代码实现补充说明图空间语法以及列子创建克隆官方示例代码(创建并克隆)USE语句指定图空间时查看所有SPACESPACE详情CLEAR SPACE删库跑路&#xff08;看玩笑的说…

什么是生成式人工智能?

近年来&#xff0c;人工智能取得了重大进展&#xff0c;其中发展迅速的领域之一就是生成式人工智能。生成式人工智能是人工智能和深度学习的一个子领域&#xff0c;主要使用机器学习技 术根据现有数据训练算法和模型&#xff0c;生成诸如图像、文本、音乐、视频等新内容。 要更…

LTD营销枢纽2023年度功能升级回顾

在过去的2023年&#xff0c;我们的团队致力于不断进步和创新。经过一年的不懈努力&#xff0c;我们共发布了50次的系统升级&#xff0c;引入了16种全新的解决方案与业务应用&#xff0c;并实施了1363项各类细致优化。 这些更新和改进不仅在我们的营销枢纽系统现有功能的基础上实…

【C++那些事儿】深入理解C++类与对象:从概念到实践(上)| 揭开this指针的神秘面纱

&#x1f4f7; 江池俊&#xff1a; 个人主页 &#x1f525;个人专栏&#xff1a; ✅数据结构冒险记 ✅C那些事儿 &#x1f305; 有航道的人&#xff0c;再渺小也不会迷途。 文章目录 1. 面向过程和面向对象初步认识2.类的引入3.类的定义4.类的访问限定符及封装4.1 访问限定符…

从零开始学PS

一、软件安装&#xff1a; 1.安装creative cloud&#xff1a; 2.下载安装PS&#xff1a; 3.下载完成&#xff1a; 二、PS主界面构成&#xff1a; 三、快捷键&#xff1a; 以下是 Photoshop 常用的 100 个快捷键&#xff1a; Ctrl N&#xff1a;新建一个文档 Ctrl O&am…

【数据结构和算法初阶(C语言)】链表-单链表(手撕详讲单链表增删查改)

目录 1.前言&#xff1a;顺序表回顾&#xff1a; 1.1顺序表的优缺点 2.主角----链表 2.1链表的概念 2.2定义一个单链表的具体实现代码方式 3.单链表对数据的管理----增删查改 3.1单链表的创建 3.2单链表的遍历实现 3.2.1利用遍历实现一个打印我们链表内容的函数的函数…

【软件测试】--功能测试4-html介绍

1.1 前端三大核心 html:超文本标记语言&#xff0c;由一套标记标签组成 标签&#xff1a; 单标签&#xff1a;<标签名 /> 双标签:<标签名></标签名> 属性&#xff1a;描述某一特征 示例:<a 属性名"属性值"> 1.2 html骨架标签 <!DOC…

Java基础八股

基础概念与常识 Java 语言有哪些特点? 简单易学&#xff1b;面向对象&#xff08;封装&#xff0c;继承&#xff0c;多态&#xff09;&#xff1b;平台无关性&#xff08; Java 虚拟机实现平台无关性&#xff09;&#xff1b;支持多线程&#xff08; C 语言没有内置的多线程…

Ansible自动化运维(四)jinja2 模板、Roles角色详解

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01; &#x1f40b; 希望大家多多支…

Retrofit核心原理

Retrofit是一个类型安全的HTTP客户端库&#xff0c;广泛用于Android和Java应用中&#xff0c;用于简化网络请求和响应的处理。本文将深入探讨Retrofit的核心原理&#xff0c;帮助开发者理解其背后的工作机制。 Retrofit简介 Retrofit是Square公司开发的一个开源库&#xff0c…