c++ double转string_第14篇:C++的string-两手抓的内存分配

本文假定你对C/C++的string语法已经有基本的了解。

  • 如果你对C++的string的内部实现原理不了解的话,请先阅读这一篇
    《对[C/C++]指针与字符串的总结》,本篇是对前一篇的内容的延伸。
  • 如果对字符串字面量不了解的话,请先阅读《C/C++中的字符字面量》。

C ++的string对象实质上就是一个容器,其内部有一个c_str方法能够返回一个指向的实质存储字符串副本的数据成员。即通过string::c_str()配合printf函数可以获取的字符串副本的内存地址。

栈中的string的内存分配

首先,我们来看看如下代码的关于string对象内部的栈中内存分配,不少C++读物强力建议在C++开发中使用标准库的string对象,而非C版本的char*指针和char[]数组。但没有详细告诉读者为什么?string对象底层都做了些什么,因此理解string内部实现原理,对于你后续使用string类实现各种字符串操作的算法非常有必要,以下代码,是前一篇文章代码的深入的演示版本

首先我们在全局作用域重载了operator new和operator delete的函数原型,内部分别用C版本的malloc和free函数,目的在于:显式展示给读者,你在使用string过程中,它已经在底层自动完成了所有的内存分配和内存释放。实际开发过程不建议这样重载operator newoperator delete

ec3d6ce8974ea2de40c160ef68c71400.png

show_str()函数是用于打印传入参数string对象str内部的字符串的地址和函数内部的局部变量的string对象tmp的内部字符串的地址。

ad78742f1a5209c472ce936eca325ded.png

下面是调用函数

2bef5f301a6fcd962ac22a7a079fc4df.png

输出结果:

首先继续进行下文之前,需要说明的是Linux下的x86_64版本的GCC/G++编译器默认情况下(编译时没有附带 -O 优化选项),仍然按照x86平台的过程调用约定组织程序栈,下文编译时使用的是默认设置。

35e3fe45d1baaff7ad154420944709df.png

从上面程序输出看来,在每次调用show_str()函数输出的内存地址看来,string对象内部持有字符串副本的内存分配都发生在程序栈帧中,有一些有趣的分析。

  • main函数我们知道string对象内部持有字符串副本的地址是"0x7ffc5b140990",输出的参数地址跟main函数中的变量you是一致的,因为我们show_str()的参数类型是const string&即使用了引用传参,我们这里避免了字符串的拷贝.
  • 每次string类型的局部变量赋值操作,string对象内部自动执行字符串拷贝,从每次打印的tmp程序地址可以得知。

匿名字符串字面量

我们第二次调用show_str()函数时,你们是否思考过如下两个问题。

  1. 0x7ffc5b1409b0从那里冒出来的,为何跟main函数的you不是一致的?
  2. 我们又没有定义新的string类型的局部变量,0x7ffc5b1409b0这个地址为什么后面会出现了两次?

首先,解答第一个疑问,从内存寻址的角度分析,一个变量必定对应于一个内存地址,也就是0x7ffc5b1409b0这个地址必定存在一个变量与之对应,但第二次调用show_str()函数,我们没有向其传入任何定义的string类型的局部变量,只是直接传入一个字符串字面量。关键就是在这里,当我们直接向show_str传入一个字符串字面量之前,C++编译器会隐式创建一个临时变量,我们假设变量的名称是任意的x。隐式的临时变量它的内部字符串副本的地址自然就指向0x7ffc5b1409b0这个地址,我们第二次调用show_str的代码,即如下代码所示

int 

接下来回答第二个问题就非常简单,由于C++已经隐式地定义了

std

那么后续调用任意的被调用函数的传参类型只要是const string&,那么传入同一个匿名的字符串字面量。自然打印的都是同一个隐式局部变量的内部字符串副本的地址。

另外比较蹊跷的是tmp每次调用show_str输出的地址是相同的,因为我们这里陆续调用的了相同show_str函数,那么show_str栈帧结构基本上一样的,如果你调用不同尺寸的函数,输出结果就会不一样。

堆中的string的内存分配

这次,我稍微做一下改动,现在我们在main中传入一个比之前更长的尺寸为33字节的字符串字面量,如下图

5b693288f22a45e632c25f61eb87aa1f.png

对应的输出

7d910ef49af6d689cf438c417ac8a81a.png

这次string对象的内存分配已经发生变化,show_str()函数中的他们的内部数据成员分别指向各自堆中分配的内存块,的字符副本分别存储这些堆中的内存块。如上图输出都分别调用了void* operator new(size_t)的重载版本。

到这里你就应该要思考两个问题

  • 为什么在处理“Hello,Word!!”只在栈中进行内存分配?
  • 为什么在处理“Hello,My name is peter!!”这样的字符串,就会在堆中进行内存分配?

没错,答案就是字符串字面量的长度决定的。这个我在前一编《对[C/C++]指针与字符串的总结》已经提到过,但当时我没有指出,触发string对象内部的new操作的准确阀值是多少。请看如下表

1753c518c1ed7ed99cfd677a3161711a.png

string对象内部约定:

  • 只要传入的字符串字面量小于上表的阀值,string内部实现在栈中分配内存,有个很骚的名字小型字符串优化(Small String Optimisation)。
  • 只要大于上述C++编译器指定阀值,string对象内部会隐式执行new操作在堆中根据指定的字符串尺寸分配初次内存
  • 如果后续任何字符串的push_back操作,string会根据“double方案”的内存分配方式对堆内存执行扩容操作,见前文《对[C/C++]指针与字符串的总结》。
  • 还有根据RAII的约定,C++编译器会对string对象在其调用函数的生命周期结束之时自动执行垃圾回收。(见上图的输出)。

建议:到这里,如果还没搞懂如下代码背后的内存含义的话,建议还是去补补栈和堆内存管理的知识,再去深入了解string对象。这样会让你少走很多弯路。

string s=new string(....)

void 

我们从内存地址的角度,分析了string对象在栈中和堆中的内存分配细节。从这篇文章你应该知道,在C++中掌握内存分析方法是多么地重要,本篇用到了以前我所写随笔的程序栈和堆内存管理的知识。

扩展阅读,如果关注我的读者应该了解我写软文的套路是一环扣一环的,可能在说string的话题,然后有跳到程序栈,这就是所谓的知识碎片整理。

  • 《第2篇:C/C++ 内存布局与程序栈》
  • 《第3篇:戏说程序栈-call指令和ret指令》
  • 《第4篇:戏说程序栈-栈帧》
  • 《第5篇-戏说程序栈-寄存器和函数状态》
  • 《第6篇-戏说程序栈 x86_64过程调用》

后记

了解string对象的行为之后,接下来我们如何考虑使用什么方法来避免字符串频繁的拷贝,有些经验的“老油条”应该都领略过了const string&这类参数类型声明并不能从根本上解决问题(上例子的程序输出已经隐藏地说明了这一点)。于是C++17就有了string_view这个标准库的扩展,这个扩展极大地解决了string拷贝的空间成本和时间成本问题。我们后续文章会继续新的话题。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值