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

作者: 铁甲万能狗 | 来源:发表于2019-11-04 16:54 被阅读0次

    本文假定你对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拷贝的空间成本和时间成本问题。我们后续文章会继续新的话题。

    相关文章

      网友评论

        本文标题:第3篇:C++的string内部原理

        本文链接:https://www.haomeiwen.com/subject/nsndyctx.html