在VS和g++下的string结构的区别

news2024/10/1 9:37:42

文章目录

      • 1. 在VS下的结构
      • 2.在gcc下的结构
      • 3.写时拷贝/共享内存

在之前的时间里,我们学习了string类的使用和模拟实现,但是在VS和g++下使用string,发现了一点问题,下面我们通过一段代码来重现一下这个问题

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s1("11111");
	string s2("22222222222222222222222222222222222");
	cout << "s1: " << sizeof(s1) << endl;
	cout << "s2: " << sizeof(s2) << endl;
	return 0;
}

这段代码在VS2022下和g++下的运行结果如下:

image-20230418001617693

image-20230418001525443

注:g++的版本为image-20230418001759447

可以看到,同样的代码,string类对象在VS下的x86环境中大小为28个字节,但是在g++下的大小仅仅为8字节,这是为什么呢?

1. 在VS下的结构

这里我们只考虑x86环境下的情况

VS下的string类对象总共占28个字节,其中的结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间

  • 当字符串长度小于16时,使用内部固定的字符数组来存放(即存放在栈上)
  • 当字符串长度大于等于16时,再从堆上开辟空间存放
union _Bxty
{ 	
    // storage for small buffer or pointer to larger one
	value_type _Buf[_BUF_SIZE];
	pointer _Ptr;
	char _Alias[_BUF_SIZE]; // to permit aliasing
}_Bx;

为什么要这样设计呢

因为在大多数情况下,创建的字符串长度都小于16,如果此时一直使用从堆上开辟的空间,然后对象生命周期结束之后再释放,会导致堆上的空间碎片化,而且频繁调用内存管理函数,导致效率很低。如果采用这种设计方式的话,将会减少很多内存管理函数的调用次数,效率高,并且不易使堆上空间碎片化

其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量

最后:还有一个指针做一些其他事情

所以string类对象共占16(buff[]的大小) + 4(char*类型的指针在x86环境下的大小) + 4 + 4 = 28个字节

image-20230418003403793

2.在gcc下的结构

在gcc下,string是通过写时拷贝实现的,string对象共占四个字节,其内部只包含了一个指针,该指针指向了一块对空间,内部包含了如下的字段:

struct _Rep_base
{
       size_type          _M_length;//字符串有效长度
       size_type          _M_capacity;//空间总大小
       _Atomic_word       _M_refcount;//引用计数
};

image-20230418012507047

3.写时拷贝/共享内存

在上文上,我们提到了**写时拷贝(Copy-On-Write)**技术。是编程界的“懒惰行为”——拖延战术的产物。

下面我们看一段代码:

int main()
{
	string s1("hello wordl");
    string s2(s1);
    printf("写时拷贝前,共享内存\n");
    printf("s1:%p\n", s1.c_str());
    printf("s2:%p\n", s2.c_str());
    s2 += '!';
    printf("写时拷贝后,内存不共享\n");
    printf("s1:%p\n", s1.c_str());
    printf("s2:%p\n", s2.c_str());
	return 0;
}

image-20230418102355157

按照我们的理解来说,s1和s2是两个不同的对象,所以两个对象的地址应该是不同的,但是我们发现在向s2中写入其他值之前,两个对象指向了同一块堆空间,这就是g++使用写时拷贝的证明。在往s2中写入新的内容之后,两个对象存放的值不同了,所以就没有办法共享内存了。

接下来有这么几个问题:

1. 写时拷贝的原理是什么?

写时拷贝使用了一个东西叫引用计数,所谓引用计数就是如果需要共享内存,那么用一个变量RefCnt来存放共享这块内存的对象个数,当RefCnt==0时,这块地址就没有对象使用,即可释放,否则就不能释放。当销毁一个对象的时候,首先判断他的RefCnt是否为0,如果不为0,那么就让RefCnt–,而不是直接销毁对象,增加一个共享内存的对象时也是同理。

2. string类什么时候才共享内存?

让我们想一下,共享内存最必要的条件是什么?是两个对象指向的内存空间中,存放的值完全相同,那么我们能想到的应该只有拷贝构造赋值重载这两种情况。

3. string类什么时候才触发写时拷贝?

显而易见,当两个对象中存放的内容相同时就共享内存,不同时就不共享内存,也就是当其中的任意一个对象指向的值发生修改时,就触发写时拷贝。例如:+=,append,insert,erase等。

4. 在写时拷贝发生时,具体发生了什么?

在问题1中,我们提到了这个方面,就是访问到RefCnt这个变量,来判断具体需要做什么,我们看下面这段代码:

if(RefCnt > 0)//有对象共享这块内存时
{
    char* tmp = new char[strlen(_str + 1)];
    strcpy(tmp, _str);
    _str = tmp;
}

上面的代码是一个假想的拷贝方法,如果有别的类在引用(检查引用计数来获知)这块内存,那么就需要把更改类进行“拷贝”这个动作。我们可以把这个拷的运行封装成一个函数,供那些改变内容的成员函数使用。

5. 写时拷贝具体时怎么实现的

在上文中,我们提到了需要有一个变量RefCnt,但是最大的问题是这个RefCnt存放在什么位置。我们要满足的情况是对于所有共享内存的对象,共享一个RefCnt,相信这句话肯定能给大家启发,我们可以把这个RefCnt存放在共享的内存中。

image-20230418105842673

于是,有了这样一个机制,每当我们为string分配内存时,我们总是要多分配一个空间用来存放这个引用计数的值,只要发生拷贝构造或赋值时,这个内存的值就会加一。而在内容修改时,string类为查看这个引用计数是否为0,如果不为零,表示有人在共享这块内存,那么自己需要先做一份拷贝,然后把引用计数减去一,再把数据拷贝过来。下面的几个程序片段说明了这两个动作

//构造函数(分存内存)
string::string(const char* tmp)
{
    _size = strlen(tmp);
    _str = new char[_size + 1 + 1];
    strcpy( _str + 1, tmp );//在数据区之前一个char用来存放RefCnt
    _str[0] = 0;//设置引用计数  
}
//拷贝构造(共享内存)
string::string(const string& str)
{
    if (*this != str)
    {
        _str = str.c_str();   //共享内存
        _size = str.size();
        _str[0]++;  //引用计数加一
    }
}
//写时才拷贝Copy-On-Write
void string::COW()
{
    _str[_size + 1]--;   //引用计数减一
    char* tmp = new char[_size + 1 + 1];
    strncpy(tmp, _str, _size + 1);
    _str = tmp;
    _str[0] = 0; // 设置新的共享内存的引用计数
}
string& string::push_back(char ch)
{
	COW();   
    if(_size == _capacity)
    {
        size_t newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
        reserve(newCapacity);
    }
    _str[_size] = ch;
    ++_size;
    str[_size] = '\0';
}
char& string::operator[](size_t pos)
{
    assert(pos <= _size || _str == nullptr);
    
	COW();
    
    return _str[pos];
}
//析构函数的一些处理
~string()
{
    if(_str[0] == 0)//引用计数为0时,释放内存
    {
        delete[] _str;
    }
    else//引用计数不为0时
    {
        _str[0]--;//引用计数减一
    }
}

写在最后

  1. 上述对写时拷贝和共享内存的讲解仅仅是原理上的讲解,和stl库中实现的可能会有所差别与简化,请忽略这些,搞懂原理即可。
  2. 这种写法终归是有炫技的成分在其中,使用时可能在某些地方出现bug,甚至使程序crash掉
  3. 在C++的使用和设计中,需要注意的细节点有很多,可能你觉得发现了一个非常巧妙的设计,但是很有可能在某些地方就会出现难以修改的bug,所以在使用C++时,一定要对原理有充分的了解。

参考博客:这里推荐陈皓大佬的写时拷贝

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

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

相关文章

融合DE 端和FE端数据,利用小波变换生成时频图,再分别利用DCNN、KNN和DNN进行对比实验(python代码)

1.数据集介绍&#xff1a; 试验台如图所示&#xff0c;试验台左侧有电动机&#xff0c;中间有扭矩收集器&#xff0c;右侧有动力测试仪&#xff0c;控制电子设备在图中没有显示。SKF6203轴承使用16通道数据采集卡采集轴承的振动数据&#xff0c;并在驱动端部分&#xff08;DE&…

AI模型训练、实施工程师的职业前景怎么样?

本篇文章主要讲解ai模型训练、模型实施工程师的职业前景和趋势分析 作者&#xff1a;任聪聪 日期&#xff1a;2023年4月18日 ai训练师、模型实施工程师&#xff0c;一般是指opencv、pytorh、python、java、机械学习、深度学习、图像识别、视频检测等领域的模型数据训练工作。 …

07 - 深度学习处理器架构⭐⭐⭐⭐

架构设计需要解决的两个主要问题:(1)如何提高处理器的能效比(性能/功耗)- 硬化算法(2)如何提高处理器的可编程性(通用性) - CPU 一、单核深度学习处理器(DLP-S) 1. 总体架构 (1)架构图 DMA是一种硬件机制,允许外围组件将其I/O数据直接传输到主存储器中,而无需…

CentOS 8 手动安装MongoDB

文章目录1. MongoDB概述2. 安装MongoDB2.1 在MongoDB官网选择对应版本2.2 去到MongoDB安装目录&#xff0c;并下载MongoDB安装包2.3 解压MongoDB安装包2.4 重命名解压后的MongoDB文件夹名2.5 创建MongoDB数据库数据存放路径2.6 创建MongoDB日志文件存放路径2.7 进入MongoDB文件…

Pixhawk基础—认识Pixhawk

Pixhawk简介 pixhawk是由3DR联合APM小组与PX4小组于2014年推出的飞控PX4的升级版&#xff0c;它同时拥有PX4和APM两套固件和相应的地面站软件。该飞控是目前全世界飞控产品中硬件规格最高的产品。 Pixhawk基础 端口介绍 1、Spektrum DSM receiver(Spektrum DSM信号转换为PWM…

Java基础总结(一)

文章目录前言封装继承多态抽象方法接口内部类static权限修饰符this superprivate关键字final关键字就近原则构造方法号StringBuilderStringJoiner字符串原理总结&#xff1a;1、字符串存储的内存原理2、号比较的是什么&#xff1f;3、字符串拼接的底层原理4、StringBuilder提高…

ASIC-WORLD Verilog(1)一日Verilog

写在前面 在自己准备写一些简单的verilog教程之前&#xff0c;参考了许多资料----asic-world网站的这套verilog教程即是其一。这套教程写得极好&#xff0c;奈何没有中文&#xff0c;在下只好斗胆翻译过来&#xff08;加了自己的理解&#xff09;分享给大家。 这是网站原文&…

Java反射面试总结(二)

为什么引入反射概念&#xff1f;反射机制的应用有哪些&#xff1f; 我们来看一下 Oracle 官方文档中对反射的描述&#xff1a; 从 Oracle 官方文档中可以看出&#xff0c;反射主要应用在以下几方面&#xff1a; 反射让开发人员可以通过外部类的全路径名创建对象&#xff0c;…

详解C语言结构体内存对齐:你知道如何快速计算结构体大小吗?

本篇博客会讲解C语言结构体的内存对齐&#xff0c;并且给出一种快速计算结构体大小的方式。主要讲解下面几点&#xff1a; 结构体的内存对齐是什么&#xff1f;如何快速计算结构体的大小&#xff1f;如何利用内存对齐节省结构体占用的内存空间&#xff1f;为什么结构体要内存对…

分布式数据库架构路线大揭秘

文章目录分布式数据库是如何演进的&#xff1f;数据库与分布式中间件有什么区别&#xff1f;如何处理分布式事务&#xff0c;提供外部一致性&#xff1f;如何处理分布式SQL&#xff1f;如何实现分布式一致性&#xff1f;数据库更适合金融政企的未来这些年大家都在谈分布式数据库…

MySQL-中间件mycat(一)

目录 &#x1f341;mycat基础概念 &#x1f341;Mycat安装部署 &#x1f343;初始环境 &#x1f343;测试环境 &#x1f343;下载安装 &#x1f343;修改配置文件 &#x1f343;启动mycat &#x1f343;测试连接 &#x1f990;博客主页&#xff1a;大虾好吃吗的博客 &#x1f9…

边缘网关thingsboard-gateway DTU902

thingsboard-gateway是一个采用python语言编写的开放源代码网关程序&#xff0c;用于将传统或第三方系统的设备与thingsboard平台连接。 支持 采集Modbus slaves、CAN、MQTT 、OPC-UA servers, Sigfox Backend。 除了具备普通 网关外&#xff0c;还具备可配置的边缘能力&…

rabbitmq深入实践

生产者&#xff0c;交换机&#xff0c;队列&#xff0c;消费者 交换机和队列通过 rounting key 绑定者&#xff0c;rounting key 可以是#.,*.这类topic模式&#xff0c; 生产者发送消息内容 rountingkey&#xff0c; 到达交换机后交换机检查与之绑定的队列&#xff0c; 如果能匹…

Yolov5之common.py文件解读

深度学习训练营原文链接前言0.导入需要的包以及基本配置1.基本组件1.1 autopad1.2 ConvDWConv模块1.3TransformerLayer模块1.4 Bottleneck和BottleneckCSPBottleneck模型结构1.5 CrossConv模块1.6 C3模块基于C3的改进1.7SPP1.8Focus模块1.9 Concat模块1.10 Contract和Expand1.1…

好东西!!!多亏几位大牛整理的面试题,让我成功上岸!!

凡事预则立&#xff0c;不预则废。相信很多程序员朋友在跳槽前都会临阵磨枪&#xff0c;在网络上搜集一些面试题进行准备。 然而&#xff0c;当机会来临时&#xff0c;却发现这些面试题往往“不快也不光”.... 由于Java面试涉及的范围很广&#xff0c;很杂&#xff0c;而且技…

使用MyBatis实现简单查询

文章目录一&#xff0c;创建数据库与表&#xff08;一&#xff09;在Navicat里创建MySQL数据库testdb&#xff08;二&#xff09;创建用户表 - t_user&#xff08;三&#xff09;在用户表里插入3条记录二&#xff0c;案例演示MyBatis基本使用&#xff08;一&#xff09;创建Mav…

解决idea每次打开新的项目都需要重新配置maven

原理&#xff1a;就是通过 idea 来进行全局配置【非当前工程配置】 IDEA 版本&#xff1a;2023.1 如何查看版本信息 &#xff1f; 【主菜单】——【帮助】——【关于】 我在网上查找了许多文章 &#xff0c;我混淆了一点&#xff01;当前工程的设置 & 全局设置 不在一个地方…

马斯克掷重金收购英

人前主义&#xff0c;人后生意。在带领一众科技圈大佬签署了呼吁暂停研发比GPT-4更强AI模型的公开信后不久&#xff0c;马斯克却转头豪掷千金收购了10000块英伟达GPU。 一些网友吐槽&#xff0c;以马老板的格局而言&#xff0c;这次价值过亿的投资绝对不是为了借着AI概念火爆来…

2021年 团体程序设计天梯赛——题解集

Hello各位童学大家好&#xff01;&#x1f60a;&#x1f60a;&#xff0c;茫茫题海你我相遇即是缘分呐&#xff0c;或许日复一日的刷题已经让你感到疲惫甚至厌倦了&#xff0c;但是我们真的真的已经达到了我们自身极限了吗&#xff1f;少一点自我感动&#xff0c;没有结果前别太…

[FREERTOS] 任务的创建、删除、调度与状态

1.什么是任务&#xff1f; 我的理解是&#xff1a;任务像是进程/线程&#xff0c;创建一个任务就会开辟一个空间&#xff0c;每一个任务都是独立的执行相应的动作互不干扰&#xff0c;就比如玩游戏&#xff0c;陪女朋友&#xff0c;任务通常都会有一个while(1)死循环 2.与任务创…