第3篇:C++的string内部原理

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

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

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

下面是调用函数


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

从上面程序输出看来,在每次调用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 main(void){
     std::string you="Hello,World!!";
     show_str(you);
     .....

     //show_str("Hello,World!!")会等价于如下代码
      std::string& x="Hello,World!!";//隐式创建
      show_str(x);
     ....
}

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

std::string& x="Hello,World!!";

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

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

堆中的string的内存分配

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

对应的输出

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

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

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

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

string对象内部约定:

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

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

string s=new string(....)

void my_app(const string &s){
      string tmp=s;
}

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

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

后记

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。