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

前言

        string是C++标准库中常见的容器,不管是在校生日常练习换是职场老鸟写项目,它都是会经常使用的一个容器,你是否经常使用它呢?你是否了解过它的底层实现呢,如果你对此感兴趣,不妨与我共同探索一下它的底层原理。

1.string对象的大小

        

#include<iostream>
#include<string>
using std::string;
using std::cout;
using std::endl;
int main(int argc, char **argv)
{string s1;string s2("asdfghjklbz");string s3("asdfghjklzxcvbnmqwertyuio134455546787980");cout << "sizeof(s1) : " << sizeof(s1) << endl;cout << "sizeof(s2) : " << sizeof(s2) << endl;cout << "sizeof(s3) : " << sizeof(s3) << endl;cout << "sizeof(string) : " << sizeof(string) << endl;while (1);return 0;
}

在vs2013下的结果:

在gcc下的结果

        为什么在不同的编译器下会存在不同的结果呢,这就要和这些编译器所采用的string的实现方式有关系了。我们接着往下看...

2.string的底层实现方式

        2.1Eager Copy

        也就是所谓的深拷贝,无论是在什么情况下,都是采取复制字符串内容的方式来进行对象的拷贝和复制。这种实现方式,对于需要对字符串需要频繁复制而又不改变字符串的内容时效率就比较低了。所以要对其采用一定的策略进行优化,之后便出现了下面这种COW的实现方式。string深拷贝的实现

        2.2COW(Copy-On-Write)

        cow也就是所谓写时复制机制。当对字符串进行拷贝和赋值时并不会进行深拷贝,而是通过增加引用计数的值,字符串只会进行浅拷贝,当对字符串中的内容进行修改时,此时才会执行真正的复制,也就是进行深拷贝。

        其实现的内存图有下面两种方式:

        第一种方式:

 

        std::string的成员就是

class string
{
private:Allocator _allocator;//空间配置器,用来管理空间,暂时不用管size_t Capacity;size_t Size;char* Point;
};

         第二种方式:

        std::string的成员包括:

 

class string
{
private:char * Point;
};

        为了实现的简单,GNU4.8.4中就采用了第二种实现方式,我们可以看到第二种实现方式并没有将引用计数直接放到string的内部而是放在了字符串内容的前面,这么做有什么好处呢?
        最显而易见的好处就是可以减少string创建对象的大小,并且对于引用计数是否可以放在栈空间上呢?放在全局/静态区是否可行呢?
        我们来一起思考一下,如果放在栈上是否能够达到写时复制的效果,首先复制的字符串和被复制的字符串中的引用计数的变化应该是同步的(要增加都增加,要减少都减少)。
        但是如果将引用技术放在栈空间中,那么复制的字符串和被复制的字符串的引用计数就无法同步变化,所以肯定不可以放在栈空间中,如果放在静态区中也是同样的道理(如果这里转不过弯来,可以好好思考一下,实在理解不了就接着往下看,看到最后你就会明白的)。
        好了我们说完了理论,下面用代码进行一下简单的实现。 

        //代码

#include<iostream>
#include<string.h>
using std::cout;
using std::endl;namespace qyy
{
class string
{
public:friend std:: ostream &operator<< (std::ostream & os, const qyy::string & rhs);string()//+1的字节是用来保存'\0'用来和C语言兼容,+ 4是用来保存引用计数的:_point(new char[1 + 4] () + 4)//用来保存引用计数{initRefCount();}string(const char* point): _point(new char[strlen(point) + 1 + 4]() + 4){initRefCount();strcpy(_point, point);//拷贝数据到新的空间中}string(const string & rhs): _point(rhs._point){//引用计数加1incrementRefCount();}string & operator= (string& rhs){if(this != &rhs){//减少原来的引用计数subRefCount();if(getRefCount() == 0){//如果此时引用计数等于0就需要将动态申请的空间进行释放clear();}_point = rhs._point;//增加新的引用计数incrementRefCount();}return *this;}//对于operator[]的访问char& operator[](size_t index){if(index < size()){//这里要思考如何区分读操作和写操作return _point[index];}else{static char nullchar = '\0';return nullchar;}}//初始化引用计数void initRefCount(){*(int*)(_point - 4) = 1;}//引用计数自增void incrementRefCount(){++*(int*)(_point - 4);}//获取引用计数int getRefCount() const{return *(int*)(_point - 4);}//引用计数自减void subRefCount(){--*(int*)(_point - 4);}size_t size() const{if(_point){return strlen(_point);}else//如果string为空此时求出来的size就是一个极大值{return -1;}}void clear(){  if(_point)//清理空间{delete [](_point - 4);_point = nullptr;}}~string(){subRefCount();if(getRefCount() == 0){clear();}}private:char * _point;
};
std:: ostream &operator<< (std::ostream & os, const qyy::string & rhs)
{if(rhs._point){os << rhs._point;}return os;
}
}

       如何区分读操作和写操作来达到写时复制呢?如果是string s1 = "hello";cout << s[1] ; 这只是一次读操作,s1[1] = 'r';这才是一次写操作,直接在operator[]中好像是无法区分的,下面是错误的演示。

 

    //对于operator[]的访问char& operator[](size_t index){if(index < size()){//这里要思考如何区分读和写操作//申请新空间char* tmp = new char[size() + 5]();//拷贝数据strncpy(tmp + 4, _point , size());//减少原来的引用计数subRefCount();clear();//清理旧的空间_point = tmp;//初始化引用计数initRefCount();return _point[index];}else{static char nullchar = '\0';return nullchar;}}

        这里提供一种思路:既然在operator[]中无法识别读和写操作,那么就在operator[]的返回值上来进行操作,将operator[]的返回值改为一个自定义类型,通过这个自定义类型来区分读写操作。 

#include<iostream>
#include<string.h>
using std::cout;
using std::endl;namespace qyy
{
class string
{
public:friend std:: ostream &operator<< (std::ostream & os, const qyy::string & rhs);string()//+1的字节是用来保存'\0'用来和C语言兼容,+ 4是用来保存引用计数的:_point(new char[1 + 4] () + 4)//用来保存引用计数{initRefCount();}string(const char* point): _point(new char[strlen(point) + 1 + 4]() + 4){initRefCount();strcpy(_point, point);//拷贝数据到新的空间中}string(const string & rhs): _point(rhs._point){//引用计数加1incrementRefCount();}string & operator= (string& rhs){if(this != &rhs){//减少原来的引用计数subRefCount();if(getRefCount() == 0){//如果此时引用计数等于0就需要将动态申请的空间进行释放clear();}_point = rhs._point;//增加新的引用计数incrementRefCount();}return *this;}//对于operator[]的访问class CharProxy{public:CharProxy(string & self, size_t index): _self(self), _index(index){}char& operator=(const char &ch);//进行写时复制操作operator char(){cout << "operator char() " << endl;return _self._point[_index];}//operator<<(char)string & _self;size_t _index;};CharProxy operator[](size_t index){return CharProxy(*this, index);}const char*c_str() const{return _point;}//初始化引用计数void initRefCount(){*(int*)(_point - 4) = 1;}//引用计数自增void incrementRefCount(){++*(int*)(_point - 4);}//获取引用计数int getRefCount() const{return *(int*)(_point - 4);}//引用计数自减void subRefCount(){--*(int*)(_point - 4);}size_t size() const{if(_point){return strlen(_point);}else//如果string为空此时求出来的size就是一个极大值{return -1;}}void clear(){  if(getRefCount() ==0 && _point)//清理空间{delete [](_point - 4);_point = nullptr;}}~string(){subRefCount();clear();}private:char * _point;
};
std:: ostream &operator<< (std::ostream & os, const qyy::string & rhs)
{if(rhs._point){os << rhs._point;}return os;
}
char &string::CharProxy::operator=(const char & ch)
{if(_index < _self.size()){char * tmp = new char[_self.size() + 5]() + 4;strcpy(tmp, _self._point);_self.subRefCount();_self._point = tmp;_self.initRefCount();return _self._point[_index];}else{static char charnull = '\0';return charnull;}
}
}

        将这个自定义类型CharProxy的operator=和 std::ostream & operator<< (std::ostream & os, const string ::CharProxy & rhs)进行实现就能区分出来读,写操作了。 需要注意的是std::ostream & operator<< (std::ostream & os, const string ::CharProxy & rhs);这个函数需要在string类和CharProxy类中都进行友元的声明,不然会报错。在CharProxy的operator=函数中进行深拷贝。

         这种实现方式可能有一定的难度对于友元函数的声明,接下来我介绍一种较为简单的实现方式。

     实现CharProxy类中的类型转换函数:operator char();

        operator char(){cout << "operator char() " << endl;return _self._point[_index];}

这样也可以达到读写分离的要求。

//测试代码

int main(int argc, char **argv)
{qyy::string s1;qyy::string s2("asdfghjklbz");qyy::string s3("asdfghjklzxcvbnmqwertyuio134455546787980");cout << "sizeof(s1) : " << sizeof(s1) << endl;cout << "sizeof(s2) : " << sizeof(s2) << endl;cout << "sizeof(s3) : " << sizeof(s3) << endl;cout << "sizeof(string) : " << sizeof(qyy::string) << endl;cout << "s1 : " << s1 << endl;cout << "s2 : " << s2 << endl;cout << "s3 : " << s3 << endl;cout << "s1.RefCount() : " << s1.getRefCount() << endl;cout << "s2.RefCount() : " << s2.getRefCount() << endl;cout << "s3.RefCount() : " << s3.getRefCount() << endl;qyy::string s4 = s2;cout << "s2.RefCount() : " << s2.getRefCount() << endl;cout << "s4.RefCount() : " << s4.getRefCount() << endl;//这里并未对s[4]的值进行修改,只是通过Operator[]函数来访问s4中的字符,//但是s4的引用计数和s[2]的引用计数确改变了cout << "s4[1]" << s4[1] << endl;cout << "s4[2]" << s4[2] << endl;cout << "s4[3]" << s4[3] << endl;cout << "s1.RefCount() : " << s1.getRefCount() << endl;cout << "s2.RefCount() : " << s2.getRefCount() << endl;cout << "s3.RefCount() : " << s3.getRefCount() << endl;cout << "s4.RefCount() : " << s4.getRefCount() << endl;s4[1] = 'i';s4[2] = 'l';cout << endl;cout << "s1.RefCount() : " << s1.getRefCount() << endl;cout << "s2.RefCount() : " << s2.getRefCount() << endl;cout << "s3.RefCount() : " << s3.getRefCount() << endl;cout << "s4.RefCount() : " << s4.getRefCount() << endl;return 0;
}

        2.3SSO

        也就是所谓的短字符串优化,一般来说,一个程序中用到的字符串大部分都是比较短的,而在64位机器上面,一个指针的就占了8个字节的空间,所以SSO就出现了,其核心思想就是发生拷贝时要复制一个指针,对于短的字符串来说如果直接复制整个字符串不是更快吗,说不一定还没有复制一个指针的代价大,对于长字符串来说就要老老实实的进行深拷贝了。如图: 

        所以std::string的成员就是: 

class string
{
private:union buf{char * _point;char _local[16];};size_t _size;size_t _capacity;
};

        当字符串的长度小于16个字节的时候,buf直接存放整个字符串;当字符串的长度大于16个字节的时候buf区域存放的就是一个指针,这个指针指向一个堆空间,堆空间中存放字符串的内容。这样做的好处就是当字符串较小的时候就不需要获取堆空间,开销小。 

3.最佳策略

        以上三种方式,都不能解决所有可能遇到的字符串的情况,它们各有所长,又各有缺陷。所以只有将它们三者的优势结合在一起,才是最优的解决方案。

        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。

        这样可以使得每个string对象占据的空间都是24个字节。某些开源库中就是这样实现的。 

4.线程安全性

        两个线程同时对同一个字符串进行操作的话,是不可能线程安全的,出于对性能的考虑,C++并没有为string实现线程安全,毕竟不是所有的程序都会使用到多线程。

        但是两个线程同时对两个独立的string操作时,必须是线程安全的,COW计数实现这一点是通过原子对引用计数进行+1或者-1操作。

        CPU的原子操作虽然比mutex锁好多了,但是任然会带来性能的损失。原因如下:

  •         阻止了CPU的乱性执行
  •         两个CPU对同一个地址进行原子操作,都导致cache失效,从而需要重新从内存中加载缓存。
  •        系统通常会lock住比目标地址更大的一片区域,影响逻辑上不相关的地址访问 

所以这些也是COW的劣势。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

监控各大电商平台商品价格,库存信息,数据分析,京东SKU详情

要接入API接口以采集电商平台上的商品数据&#xff0c;可以按照以下步骤进行&#xff1a; 1、找到可用的API接口&#xff1a;首先&#xff0c;需要找到支持查询商品信息的API接口。这些信息通常可以在电商平台的官方文档或开发者门户网站上找到。 2、注册并获取API密钥&#x…

使用IDEA官方docker插件构建镜像

此方法同样适用于jetbrains系列的其他开发软件 在IDEA中&#xff0c;如果是maven项目&#xff0c;可以使用插件 <plugin><groupId>com.spotify</groupId><artifactId>docker-maven-plugin</artifactId><version>1.2.2</version> &…

神经网络框架的基本设计

一、神经网络框架设计的基本流程 确定网络结构、激活函数、损失函数、优化算法&#xff0c;模型的训练与验证&#xff0c;模型的评估和优化&#xff0c;模型的部署。 二、网络结构与激活函数 1、网络架构 这里我们使用的是多层感知机模型MLP(multilayer prrceptron)&#x…

Docker中容器的相关操作(一)

1.查看容器 docker ps&#xff1a;查看当前docker引擎中正在运行的容器。 docker ps -a&#xff1a;查看所有的容器(包括运行中的和停止的)。 docker ps -aq&#xff1a;查看所有的容器id。 2.运行容器 错误举例&#xff1a;docker run 镜像名/镜像id 注意&#xff1a;这种…

Python 雷达图的绘制(极坐标图) (Matplotlib篇-14)

Python 雷达图的绘制(极坐标图) (Matplotlib篇-14)         🍹博主 侯小啾 感谢您的支持与信赖。☀️ 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹…

MySQL 管理工具

1、MySQL 管理 系统数据库 a. mysql 命令 语法&#xff1a;mysql [options] [database] -u,--username 指定用户名-p,--password[name] 指定密码-h, --hostname 指定服务器IP或域名-P, --portport 指定连接端-e,--executename 执行SQL语句并退出 mysql -h192.168.200.202 -…

数据结构入门到入土——链表(1)

目录 一&#xff0c;顺序表表/ArrayList的缺陷 二&#xff0c;链表 三&#xff0c;链表的实现 四&#xff0c;与链表有关的题目练习&#xff08;1&#xff09; 1.删除链表中等于给定值 val 的所有节点 2.反转一个单链表 3.给定一个带有头结点 head 的非空单链表&#xf…

【Java】RuoYi-Vue-Plus 多数据源整合TDengine时序数据库——服务端自动建库建表

目录 环境准备整合TDengine 数据源1. 添加驱动依赖2. 添加数据源配置3. 添加Mapper4. 添加建表sql脚本5. Controller 测试效果 环境准备 RuoYi-Vue-Plus v5.1.2JDK17Maven 3.6.3Redis 5.XMySQL 5.7TDengine 2.6.0.34 客户端 整合TDengine 数据源 1. 添加驱动依赖 注意&…

基于ssm杂志在线阅读网站论文

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装杂志在线阅读网站软件来发挥其高效地信息处理的作用&#x…

Linux ssh 实现远程免密登录

一、背景 我搭建了一个 zookeeper 集群&#xff0c;写了一个 shell 脚本来控制集群的启动和关闭&#xff0c;但是我发现每次我执行 shell 脚本的时候&#xff0c;都需要我输入各个服务器的密码才可以运行&#xff0c;感觉很麻烦。shell 脚本里面连接其他服务器用的就是 ssh 的方…

C#之反编译之路(一)

本文将介绍微软反编译神器dnSpy的使用方法 c#反编译之路(一) dnSpy.exe区分64位和32位,所以32位的程序,就用32位的反编译工具打开,64位的程序,就用64位的反编译工具打开(个人觉得32位的程序偏多,如果不知道是32位还是64位,就先用32位的打开试试) 目前只接触到wpf和winform的桌…

shell 获取主机IP 脚本中使用IP 打印出来IP

目录 查看ip查看网关只查看ens33过滤ip拿到网卡名字对内容进行切片获取第一网卡IP &#xff08;head -1 表示第一个网卡&#xff09;脚本中打印IP 查看ip ip a 查看网关 ip r只查看ens33 ip a show dev ens33过滤ip ip a | grep -oE [0-9]\.[0-9]\.[0-9]\.[0-9] 拿到网卡名…