ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

【C++】STL简介 及 string的使用详解

2026/8/8 17:35:34 拓冰建站 浏览量
【C++】STL简介 及 string的使用详解 文章目录1. STL简介1.1 什么是STL1.2 STL的版本1.3 STL的六大组件2. string类的使用2.1 C语言中的字符串2.2 标准库中的string类2.3 string类的常用接口说明1. string类对象的常见构造2. string类对象的容量操作3. string类对象的修改操作4. resize和reserve5. 认识迭代器正向6. 反向迭代器7. const迭代器正向反向8. 元素访问9. insert和erase10. replace、find、rfind、substr11. swap与string::swap12. c_str13. getline2.4 总结1. STL简介1.1 什么是STLSTL(standard template libaray-标准模板库)是C标准库的重要组成部分不仅是一个可复用的组件库而且是一个包罗数据结构与算法的软件框架。1.2 STL的版本原始版本Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本本着开源精神他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码无需付费。唯一的条件就是也需要向原始版本一样做开源使用。HP 版本——所有STL实现版本的始祖。P. J. 版本由P. J. Plauger开发继承自HP版本被Windows Visual C采用不能公开或修改缺陷可读性比较低符号命名比较怪异。RW版本由Rouge Wage公司开发继承自HP版本被C Builder 采用不能公开或修改可读性一般。SGI版本由Silicon Graphics Computer SystemsInc公司开发继承自HP版 本。被GCC(Linux)采用可移植性好可公开、修改甚至贩卖从命名风格和编程风格上看阅读性非常高。我们后面学习STL要阅读部分源代码主要参考的就是这个版本。1.3 STL的六大组件这个大家先了解一下我们后面都会慢慢的进行学习。2. string类的使用2.1 C语言中的字符串C语言中字符串是以’\0’结尾的一些字符的集合为了操作方便C标准库中提供了一些str系列的库函数但是这些库函数与字符串是分离开的不太符合OOP的思想而且底层空间需要用户自己管理稍不留神可能还会越界访问。在OJ中有关字符串的题目基本以string类的形式出现而且在常规工作中为了简单、方便、快捷基本都使用string类很少有人去使用C库中的字符串操作函数。2.2 标准库中的string类那标准库中的string到底是个啥呢它其实是一个类模板实例化出来的一个模板类。string类的文档介绍我们可以看到它其实是basic_string这个类模板实例化出来的类的一个typedef。ps这个页面翻译有些地方可能不恰当。可以看到basic_string实例化出来的模板类除了string还有三个。它们都是basic_string这个类模板实例化出来的模板类区别在于它们对应的模板参数的类型不同。那对于这个string类呢其实它的底层就是一个动态的字符数组就像我们之前数据结构写的顺序表。那string呢就是一个char类型的字符数组wstring就是对应的wchar_t的字符数组u16string就是char16_t的字符数组u32string就是char32_t的字符数组。那这些不同类型的字符对应的大小也是不同的。欸那大家现在有没有一个疑问为什么搞出这么多种的string类呢那我们在C语言阶段有了解过ASCII编码这里面的所有符号和字母都一个对应的ASCII码值。那问一下大家假如现在我们要存一个字符串char str[] hello;那它在内存中存的是啥我们看到内存里存的并不是字母本身而是它们对应的ASCII码值这里以16进制显示。那我们去打印的时候呢其实它也是去对照这个表找到这个ASCII码对应的字母然后显示。所以呢ASCII其实主要是来显示英语这些语言的。那这样的话随着计算机的发展只有一个ASCII编码还够用吗是不是就不行了啊因为世界上还有很多国家很多种语言呢。比如现在我们要让计算机能显示中文用ASCII码是不是就不行了啊。而且ASCII只定义了128个字符一个字节就够用了中国的汉字大约有10万个呢那基于这样的原因呢有人就又发明了Unicode——万国码兼容ASCII但是呢各个国家的情况也不同有的国家文字少有的多所以Unicode又进行了划分分为UTF-8、UTF-16、UTF-32这些。所以呢为了应对这些不同的编码就产生了这些不同的字符类型所以就有了basic_string这个泛型字符串类模板我们可以用它实例化出不同类型的字符串类。那这里面最常用的呢其实还是string。string是表示字符串的字符串类该类的接口与常规容器的接口基本相同再添加了一些专门用来操作string的常规操作。string在底层实际是basic_string模板类的别名typedef basic_stringchar, char_traits, allocator string;不能操作多字节或者变长字符的序列。在使用string类时必须包含#include头文件以及using namespace std2.3 string类的常用接口说明1. string类对象的常见构造(constructor)函数名称功能说明string() 空字符串构造函数默认构造函数构造一个空字符串长度为零个字符string (const char* s)用一个常量字符串来构造字符串类对象string (const string str, size_t pos, size_t len npos) 用的不多复制 str 中从字符位置 pos 开始并跨越 len 字符的部分如果 str 太短或 len 是stringnpos则直到 str 的末尾string (const char* s, size_t n)拿s指向字符串的前n个字符去构造string对象string (size_t n, char c)拿n个字符c去构造string对象string (const string str)拷贝构造templateclass InputIteratorstring (InputIterator first, InputIterator last)涉及到迭代器后面再说先来看string()构造一个空字符串。string (const char* s)另外呢这里还支持这样写那这个我们之前是不是讲过啊单参数的构造函数是支持隐式类型转换的。string (const string str, size_t pos, size_t len npos)这个怎么用呢它其实是拿str中的一个子串去去构造string对象这个字串是从str中下标pos位置开始长度为len的一个字串。那这个地方还说了如果这里的str比较短或者这里给的len是string::npos则这个字串一直到str的末尾。什么意思呢举个栗子我们现在的len是50那这时字符串的长度是不是不够啊比50短那这个时候怎么办会报错了不会的这里它会取到字符串的结尾位置那我们看到这里还说如果给的len是string::npos也会一直到str末尾而且我们发现这里的参数len给的是有缺省值的而这个缺省值就是npos那这个npos是个啥呢我们看到它是一个静态成员变量值是-1但是呢因为这里它的类型是size_t无符号整型所以它在这里其实是整型的最大值而我们的字符串长度是不可能大于这个值的所以这里也是会取到结尾。这个其实用的不是很多但这里第一次见带大家了解一下。string (const char* s, size_t n)拿s指向字符串的前n个字符去构造string对象string (size_t n, char c)拿n个字符c去构造string对象string (const string str)拷贝构造2. string类对象的容量操作总共呢有这么多。首先我们看到有个size还有个length都是返回字符串长度。欸那他们俩的功能一样为什么要搞两个呢搞一个size搞一个length。那这里呢其实跟一些历史原因有关string呢其实出现的比STL早string其实严格来说是不属于STL的它是C标准库产生的在STL出现之前就已经在标准库出现了。那string呢其实最早之前设计的就是length因为字符串的长度嘛用length就很合适。但是后面STL出现之后里面的其它数据结构用的都是size那为了保持一致就给string也增加了一个size。所以size()与length()方法底层实现原理完全相同引入size()的原因是为了与其他容器的接口保持一致一般情况下基本都是用size()。然后我们看到有一个max_size它的作用呢是返回字符串的最大长度但是呢真正在实际中字符串可以并不能开这么长而且在不同平台下这个值也可能不一样。所以这个东西大家了解一下知道有这么个东西就行了。然后我们来看一下capacitycapacity呢其实就是返回当前string对象的容量即当前给它分配的空间有多大我们之前学过数据结构相信这个大家很好理解。我们看到这里返回的s的容量是15但是呢这里想告诉大家这里它是不包含给\0的空间的因为它认为\0不是有效字符所以这里实际上是16个字节的空间。我们可以调式观察一下然后其它的呢大家可以自己结合文档先了解一下我们后面再详细一点去解释剩下的一些比较重要的。3. string类对象的修改操作ps有的我们放在比较后面一点讲还有的不重要的我们就简单了解一下。先来看一下p ush_back顾名思义p ush_back就是尾插嘛。演示一下那这是尾插或者说追加一个字符那要是想追加一个字符串呢也是可以的不过这里不再用push_back提供了另一个接口——append然后这个append它也是重载了一大堆的版本但有的其实都不怎么用所以string的设计其实是被吐槽过的有些地方设计的不是很好。那最常用的呢其实还是直接去追加一个字符串但是呢其实平常我们并不喜欢用push_back和append。而是去用string还重载了用起来就非常爽字符字符串都可以不过其实的底层也是用的push_back和append对他们进行了一层封装。4. resize和reserve那现在我们再回过头来看一下容量中的resize和reserve首先我们来观察一个东西就是我们定义一个string对象我们观察一下在不断插入数据的过程中它是如何进行扩容的。那这里已经写好了一个程序intmain(){string s;size_t szs.capacity();coutmaking s grow:\n;coutcapacity changed: sz\n;for(inti0;i100;i){s.push_back(c);if(sz!s.capacity())//!就证明扩容了{szs.capacity();coutcapacity changed: sz\n;}}return0;}我们运行一下我们看到是这样一个情况。首先我们上面说过了嘛他这里没有算\0的空间所以这里看到的是15实际是16个空间31实际是32 好吧。那这样的话我们看到它好像第一次扩容是2倍扩后面每次都差不多是一个1.5倍扩。但实际呢想告诉大家其实在VS上它这个结构跟我们理解的顺序表还是有一点不同其实刚开始的数据并没有存到动态开辟的数组上存到了一个自己的数组里面。我们可以调式观察一下我们看到它是存到这个Buf数组里面了这个数组的大小是16不带\0就是15所以如果string对象的大小16就存到这个Buf数组上大于16才存到Ptr指向的动态开辟的数组上就不往Buf里面存了。可以认为它的结构是一个类似这样的所以如果Buf 满了它第一次在堆上开空间就开32 字节然后后面是1.5倍增长扩容所以不能说第一次扩了2倍。那我们可以验证一下我们打印它的大小发现是28个字节如果只有指针ptrsize和capacity的话应该是12字节那现在是28就是因为它这里还有一个大小16的数组那这样做的话小块的空间就可以不去堆上开辟了如果比较大需要去堆上开辟的话起始就是32字节然后不够再扩。再来看现在i为100的时候我们看到这时的字符串就存到Ptr指向的动态开辟的空间上了就不再用Buf存了。所以我们可以认为string的扩容是1.5倍去扩的那在所有地方都是这样吗不是的在我们目前的Vs上是这样不同平台的实现可能就不一样的。我们可以看一下在Linux的g下是2倍扩容的而且这样看的话它是没有Buf数组的。那这里了解了这个扩容的机制之后我们真正想给大家讲的一个东西是什么呢其实是想给大家说一下这个reserve和resize。那上面我们看到如果我们一直插入数据他是会去不断扩容的那其实我们是有方法去减少扩容的。如果我们知道要插入多少数据的话我们可以去调这样一个接口——reserve注意不是逆置reverse那reserve的作用是什么呢reserve可以帮助我们更改容量大小这样如果我们知道需要多大的空间就可以一次开到位就不用再一次一次的扩容了。就拿我们上面那个例子来说我们现在直接reserve100个容量但是注意我们指定100它不一定开的就是100可能由于对齐啊等等的一些原因它会给你多开一些空间但是肯定不会比100小。这次大家看还有没有扩容啊是不是就没有了这里直接开了111比100多了一些。Linux下呢我们看到就是给了100这就是它们底层实现的机制可能不一样就有一些差异。所以呢如果我们知道需要多少空间的前提下reserve就可以帮助我们提前把空间开好然后就可以减少扩容提升效率因为频繁扩容也是需要付出代价的。那还有一个resize它的作用是什么呢我们说reserve可以去改变容量帮我们开空间那resize呢不仅可以开空间而且还能对开好的空间进行初始化。另外大家要知道reserve只是开空间改变容量它是不会改变size的而resize呢我们看到capacity和size都变了因为它是会对开好的空间进行初始化的相当于插入了新字符所以size也变了这里我们没有指定第二个参数既要填入的字符默认给的是\0当然我们也可以自己指定要填入的字符当然我们刚才传的第一个参数n是大于当前字符串长度的那么他就去扩容如果我们传的n小于当前字符串长度它还可以帮我们删除多出来的内容那大家思考一下这样做的话会改变capacity吗我们看到只是size变了capacity并没有改变。因为一般情况下是不会轻易缩容的缩容的话一般是不支持原地缩的我们之前学习realloc扩容有原地扩和异地扩两种方式而且原地扩也是有条件的后面要有足够的空间才能原地扩。而缩容呢可以原地缩吗由于底层内存管理的一些原因是没法原地缩的。如果支持原地缩是不是就要支持释放一部分我们申请一块空间不用了只释放其中的一部分。但是是不支持只释放一部分的就像我们free是不是要求传的指针必须是指向起始位置的。所以如果真的要缩容的话只能异地缩就是开一块新的小空间把需要的数据拷贝过去然后把原空间释放掉。所以缩容是要付出性能的代价的系统原生是不支持的我们需要自己去搞。所以不到万不得已不要轻易缩容。不过其实string是提供了一个可以缩容的接口的——shrink_to_fit但是它是要付出代价的所以我们要谨慎使用。5. 认识迭代器正向那现在大家思考一个问题如果我们想遍历一个string对象可以有哪些方式首先可以循环用[ ]遍历因为string是重载了[ ]的或者我也可以用范围for。那除了这些方法之外呢我们还可以用迭代器。举个栗子intmain(){strings1(hello world);string::iterator its1.begin();while(it!s1.end()){cout*it ;it;}return0;}解释一下首先这里的it就是我们定义的一个string类的迭代器string::iterator是类型那这么理解迭代器这个东西呢现阶段呢大家可以认为它是一个像指针一样的东西不一定是指针。那这里的begin呢会返回指向字符串第一个字符的迭代器。end返回指向最后一个字符后面位置的迭代器。我们就可以理解成指向这个位置的指针那这样我们去循环遍历解引用it就可以遍历到整个string对象。那这样对比一下的话是不是用范围for会比较爽一些那这里想告诉大家的是范围for看起来好像很牛逼但是其实它的底层也是用的迭代器。6. 反向迭代器那迭代器除了像上面那样支持正向从前向后遍历其实还可以倒着遍历倒着遍历的叫做反向迭代器。我们看到除了begin和end这里还有rbegin和rend它们返回的是reverse iterator即反向迭代器。那rbegin和rend返回的是什么呢string s1(hello world);那还拿这个对象举例子大家就可以理解为rbegin是指向字符d的但实际实现不一定是这样rend是指向字符h的前一个那我们来用一下那大家先思考一下这个地方rit应该还是- -还是大家可能认为这里从后往前倒着走应该是- -了。不要这样理解。大家想正向迭代器是往后走那反向迭代器就是方向相反了那不就是往前走了嘛。我们验证一下是不是就反向遍历了。7. const迭代器正向反向那大家再来看这样一个场景我们把s1传给一个函数然后在函数里面用迭代器遍历打印它但是这里报错了说不存在什么到什么的适当转换。为什么呢我们看到函数func的形参s是s1的引用但是加了const修饰也就是说与我们上面写的代码的区别在于这里的string对象即func中的s是const对象。那s是const对象为什么这里就不行了呢const对象是不是就不能被修改了啊那我们上面讲普通迭代器的时候说了可以认为它是一个像指针一样的东西那我们对它解引用是不是就可以修改它了所以这里我们就不能用普通迭代器了这样是不是就权限放大了所以这里才报错了。那怎么解决呢我们看到begin是有两个版本的如果是const对象调用begin那么返回的是const迭代器const_iterator普通迭代器可以读容器的数据也可以去修改但是const迭代器就只能读不能修改。所以这里s调用begin返回的是const迭代器我们用const迭代器迭代器接收就行了。当然const迭代器我们是不能去修改的不能修改它指向的内容其本身可以修改。那同样的道理普通的迭代器有正向和反向那const迭代器就也有正向和反向的两个版本。刚才我们上面的就是正向即const对象调用begin和end返回的迭代器。那const反向迭代器就是const对象调用rbegin和rend返回的迭代器——const_reverse_iterator我们来试一下当然这里我们看到迭代器的类型是不是有点长啊那我们可不可以简化一下呢是不是可以用auto啊我们说auto是不是可以自动推导类型啊但是如果你明确知道这里返回的是什么类型写成auto可以简便一点那如果给不知道的人看可能会有点懵。所以说它并不是一个完全完美的东西。那还要给大家提一下的就是我们看到这里C11又提供了一套迭代器cbegin cend crbegin crend它们只返回const迭代器。为什么搞出这些呢它是这样想的我们上面讲的迭代器比如都是调用begin普通对象调用返回普通迭代器const对象调用返回const迭代器好像有点不清楚它期望你普通对象就去调上面我们讲的不带c的那一套const对象就调带c的那一套规范一点。但是呢好像不是很必要而且大家一般也不太喜欢用这些新的所以这个大家了解一下就行了。8. 元素访问那首先呢就是[]string是重载了[]的我们可以直接用然后这里想跟大家说的是operator[]也是有普通版本和const版本的普通对象调[]就返回char可以去修改它const对象就返回const char不能修改。然后我们看到还有一个atat作用跟[]是一样的而且它同样也有const和非const版本但是呢它们两个还是有区别的区别在于用[]如果越界访问的话是直接报错的它内部是断言去判断的但是at呢我们看到是这样的它其实是抛了个异常异常是可以捕获的但是现在我们还没学大家先了解一些就行了。但是在实际当中其实我们很少会用at。然后还有一个back和front其实就是返回最后一个和第一个字符但是这个我们用[]就能搞定所以大家简单了解一下就行了。9. insert和erase那到这里我们再回过头来看几个之前跳过的接口。先来看一下insert借助insert我们可以向string对象中插入字符和字符串我们看到这里也是提供了好多版本但是有的并不常用。那我们来练习几个比较常用一点的。现在有一个string对象s那我们现在想在world前面插入一个字符串hello怎么搞那我们就可以考虑用这个第一个参数指定我们要从哪个位置开始插入第二个参数指定要插入的字符串然后我们又想在hello后面插入一个空格怎么搞首先可以这样或者我们可以去调这个除此之外还有这个接口我们看到这个不是传下标而是传目标位置的迭代器这样就可以了。那然后大家思考一个问题对于string来说大家觉得可以频繁使用insert吗或者说经常用insert好不好那我们其实是不推荐经常使用insert的为什么呢我们说了string底层是字符数组那我们学过数据结构知道在顺序表里插入元素是不是要挪动数据啊效率是比较低的所以呢insert我们能少用就少用。与insert对应接下来我们看一下erase那erase呢其实就是去删除string对象里的元素。举个栗子现在我们想删除s里面hello后面的空格怎么搞可以用这个从第5个位置开始删除一个字符然后我们看到这里len的缺省值是nposnpos我们之前是不是说过啊在这里是整型最大值所以这里如果我们传的len比字符串长度大或者不传它都会删除到字符串结尾。还可以用这个传迭代器的位置然后还有一个接口涉及到迭代器区间我们这里先不说。那同样的道理对于erase来说如果我们只删除中间的一部分是不是也要挪动数据啊所以erase也不推荐频繁使用。10. replace、find、rfind、substr我们再来了解一下replacereplace其实就是可以把字符串中的一部分替换成新内容。也有很多版本我们不可能全部都讲必要时大家可以自己查阅文档。我们看这样一个场景现在想把s里面的空格替换成%%d怎么搞确实完成了但是大家看这样做真的好吗有什么问题首先可能会需要挪动数据我们当前这个例子就有其次空间如果不够还得扩容所以这个东西我们也尽量避免去用。然后我们再来看一下findfind可以在字符串里查找字串或者字符返回对应的下标。找不到返回npos举个栗子在s里查找空格怎么搞当然也可以查找字符串然后我们还看到它还给了一个缺省参数pos缺省值是0其实pos呢是用来指定我们开始查找的位置的我们自己不指定那就默认从0开始我们指定了就从指定的位置开始找。我们可以试一下大家看这个我们要找ll但是我们指定从下标5的位置开始找那是不是就找不到了所以返回npos打印出来的就是npos对应的值。那我们来看这样一个问题假设现在有一个文件名的字符串比如说string.cpp我们想把后缀名取出来怎么搞这时我们就可以考虑使用substr这个成员函数。它的作用是什么呢它可以帮助我们获取string对象中指定的一个子串。参数那有了substr我们就可以怎么解决上面的问题我们是不是可以先用find找的.的位置然后计算出后缀名的长度len从.的位置开始向后获取长度为len的字串。intmain(){strings(string.cpp);size_t poss.find(.);if(pos!string::npos){string sufs.substr(pos,s.size()-pos);coutsufendl;}return0;}运行测试一下这不就拿到了嘛。那再看如果是这种情况呢它有多个后缀但是我们只取最后一个这下应该怎么办那除了我们上面学过的find还有rfind那rfind和find有什么区别呢区别在于find是从前往后找第一个匹配项而rfind是从后往前找倒数第一个匹配项。所以当前这种情况我们是不是把find换成 rfind就行了啊当然其实刚才这两个场景我们使用substr可以不传第二个参数的这里给了缺省值npos也就是我们不传的话它默认从我们给的位置一直取到结尾。然后呢我们看到substr后面还有一个compare就是进行string对象之间的比较嘛但是这个compare我们一般用不上。因为string还重载了关系运算符我们可以直接用来比较但是这里我们看到光是它就重载了3个版本但是其实根本没必要这也是被吐槽的一个点这张截图取自陈皓大佬的一篇博客STL 的string类怎么啦11. swap与string::swapstring里面呢还有一个成员函数叫做swap它可以接收一个string对象与当前对象进行交换。演示一下那除了这个swap之外我们之前不是提到过说C的库里面也有一个swap它是一个模板函数。所以我们也可以这样交换两个string对象也可以完成交换。那大家来思考一下这两个swap哪一个效率更高一点那这里肯定是string的swap是更高效的一点的因为stringswap作为string的成员函数那它里面想交换这两个对象就可以怎么办是不是可以直接改变指针的指向交换它们两个的成员变量啊而库里面的这个swap函数模板是怎么交换的我们看到这里是构造一个临时变量然后又有两个赋值而string的拷贝是需要深拷贝的所以它这里是比较低效的。当然即使我们这些写其实也不会调到这个因为这里提到了还存在一个非成员函数的swap使用与此成员函数类似的优化所以即使我们写成swap(s1,s2);也不会调模板函数而是调这个已经实例化好的更加匹配的。12. c_str下面我们再来看一个string的成员函数叫做c_str那它的作用是什么呢它其实是去返回一个指向当前string对象对应的字符数组的指针类型为const char*。所以如果我们想打印一个string对象就可以有这样两种方式你可以认为c_str返回的指针就类似于这里的指针p那这里第一个打印其实调的是string重载的第二个调的就是库里面的那个。那上面那个例子我们看到两种方式打印出来没什么区别那如果这样呢intmain(){strings1(hello world);s1\0;s1\0;s1*******;couts1endl;couts1.c_str()endl;return0;}这时我们再用这两种方式打印我们看到结果就有区别了。为什么呢因为第一种方式我们直接打印string对象s1它是去看s1对应的size 的size是多大总共有多少字符全部打印完。但是我们第二种打印c_str返回的const char*的指针它是遇到\0就停止了。所以大家可以理解成c_str就是返回C格式字符串。所以这次就有差异了。13. getline我们来看这样一个场景intmain(){string s;cins;coutsendl;return0;}这里定义了一个string对象现在我们要自己给它输入一个值然后打印输出。我们来试一下嗯但是我们发现这里出现了一点问题。我们输入的是hello world但是为什么打印s出来只有hello啊怎么回事大家要知道C语言里的scanf包括这里的cin我们在用它们输入的时候是不是有可能输入多个值啊那当我们输入多个值的时候它们默认是以空格或者换行来区分我们输入的多个值的。所以我们这里输入的hello world会被认为是两个值以空格分隔开了所以cin值读到了空格前面的hello后面的world就被留在缓冲区了。那这种情况怎么解决呢我们就可以用一个函数叫做——getlinegetline呢它读取到换行才结束当然它还支持我们自己指定结束符。第一个参数接收一个iostream的输入流对象如 std::cin或 std::ifstream 等文件流表明你要从哪个输入流中去读取数据我们这里要从键盘标准输入读所以就传cincin 是 C 标准库中定义的标准输入流对象它封装了标准输入设备通常是键盘第二个参数接收我们要输入的string对象。我们试一下这下就可以了。2.4 总结那我们简单总结一下我们这篇文章关于string使用的讲解差不多就到这里了string提供的接口是比较多的我们不可能全部都讲完当然其中大部分我们平时可能都不会怎么用到常用的其实是比较少的有些没讲到的后续如果大家有需要可以查阅文档进行学习——链接: link那我们这篇文章就到这里欢迎大家指正后续我们还会对string进行模拟实现到时候有些地方我们或许能够理解的更深刻一点。