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

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

1. STL简介

1.1 什么是STL

STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。

416457ff5eff45959c039081b0610fa3.png

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 Systems,Inc公司开发,继承自HP版 本。被GCC(Linux)采用,可移植性好,可公开、修改甚至贩卖,从命名风格和编程风格上看,阅读性非常高。

我们后面学习STL要阅读部分源代码,主要参考的就是这个版本。

1.3 STL的六大组件834afe0cd896408b9b8aad20c5f15f87.png


这个大家先了解一下,我们后面都会慢慢的进行学习。

2. string类的使用

2.1 C语言中的字符串

C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。

在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。

2.2 标准库中的string类

那标准库中的string到底是个啥呢?

🆗,它其实是一个类模板实例化出来的一个模板类。

string类的文档介绍

5b892aec063b4de48d4be65aa3637631.png

我们可以看到,它其实是basic_string这个类模板实例化出来的类的一个typedeffc3d7fc62187410b9f488a534fa904a3.png

ps:这个页面翻译有些地方可能不恰当。

6802a1aa4e6e4a8894c9eec147b366b2.png

9777930dde264c2b9f26d24c933e384d.png

可以看到,basic_string实例化出来的模板类除了string还有三个。

6269f18e4b5b4aecb60a5dc8dc9974a3.png

它们都是basic_string这个类模板实例化出来的模板类,区别在于它们对应的模板参数的类型不同

那对于这个string类呢?

其实它的底层就是一个动态的字符数组,就像我们之前数据结构写的顺序表。

那string呢就是一个char类型的字符数组,wstring就是对应的wchar_t的字符数组

7e78ad060edc4e7ca77d429cda0e9b74.png

u16string就是char16_t的字符数组,u32string就是char32_t的字符数组。

aed0ea37f6c1492fa0a788242f00a9bf.png

那这些不同类型的字符对应的大小也是不同的。

欸!那大家现在有没有一个疑问,为什么搞出这么多种的string类呢?

🆗,那我们在C语言阶段有了解过ASCII编码:

image.png

这里面的所有符号和字母都一个对应的ASCII码值。

那问一下大家假如现在我们要存一个字符串

char str[] = "hello";

那它在内存中存的是啥?

5441308013714beeb5268e8993fbe7f7.png

我们看到内存里存的并不是字母本身,而是它们对应的ASCII码值(这里以16进制显示)。

那我们去打印的时候呢其实它也是去对照这个表找到这个ASCII码对应的字母然后显示。

所以呢

ba1167f02cd44c67acb57cfb71a2a3e9.png

ASCII其实主要是来显示英语这些语言的。

那这样的话,随着计算机的发展,只有一个ASCII编码还够用吗?

是不是就不行了啊,因为世界上还有很多国家,很多种语言呢。比如现在我们要让计算机能显示中文,用ASCII码是不是就不行了啊。而且ASCII只定义了128个字符(一个字节就够用了),中国的汉字大约有10万个呢!

那基于这样的原因呢,有人就又发明了Unicode——万国码(兼容ASCII):

7c0a2266e5b745329db789d2bf018913.png

但是呢各个国家的情况也不同,有的国家文字少,有的多,所以Unicode又进行了划分,分为UTF-8、UTF-16、UTF-32这些。

411a5dd4ca9c4e8182aba0b61f1bb4f9.png

所以呢,为了应对这些不同的编码,就产生了这些不同的字符类型,所以就有了basic_string这个泛型字符串类模板,我们可以用它实例化出不同类型的字符串类。

🆗,那这里面最常用的呢其实还是string。


string是表示字符串的字符串类

该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。

string在底层实际是:basic_string模板类的别名,typedef basic_string string;

不能操作多字节或者变长字符的序列。

在使用string类时,必须包含#include头文件以及using namespace std

2.3 string类的常用接口说明

1. string类对象的常见构造

7ca23ddf16cb4613996bc2f6b36fa769.png

(constructor)函数名称 功能说明

string() 空字符串构造函数(默认构造函数) 构造一个空字符串,长度为零个字符

string (const char* s) 用一个常量字符串来构造字符串类对象

string (const string& str, size_t pos, size_t len = npos) (用的不多) 复制 str 中从字符位置 pos 开始并跨越 len 字符的部分(如果 str 太短或 len 是string::npos,则直到 str 的末尾)

string (const char* s, size_t n) 拿s指向字符串的前n个字符去构造string对象

string (size_t n, char c) 拿n个字符c去构造string对象

string (const string& str) 拷贝构造

template string (InputIterator first, InputIterator last) 涉及到迭代器,后面再说

先来看string():

37817fcc974b410381380d4811c86470.png

构造一个空字符串。

string (const char* s)

ab96486712b1499c8d37d2cb915895a1.png

另外呢,这里还支持这样写:

cbd43b0b9b584ea6a5ce5277581aa369.png

那这个我们之前是不是讲过啊,单参数的构造函数是支持隐式类型转换的

f38dc98451b04126a53b7c270f704966.png

string (const string& str, size_t pos, size_t len = npos)

这个怎么用呢?

它其实是拿str中的一个子串去去构造string对象,这个字串是从str中下标pos位置开始,长度为len的一个字串。59eb6e84220b48baac559b39075010ea.png

那这个地方还说了,如果这里的str比较短,或者这里给的lenstring::npos,则这个字串一直到str的末尾

什么意思呢?

举个栗子

e3e449fe479545bc8903664369976996.png

我们现在的len是50,那这时字符串的长度是不是不够啊,比50短,那这个时候怎么办,会报错了?

不会的,这里它会取到字符串的结尾位置:

1a04f01137a04f41a02d356b9bce0ae6.png

那我们看到这里还说如果给的len是string::npos,也会一直到str末尾,而且我们发现:

08c16ffc24394f8bbb358c94f905701f.png

这里的参数len给的是有缺省值的,而这个缺省值就是npos,那这个npos是个啥呢

1a47fc7d9f4d430f98bb051461870f1c.png

我们看到它是一个静态成员变量,值是-1,但是呢,因为这里它的类型是size_t(无符号整型),所以它在这里其实是整型的最大值

fc2984d8015f4b40966995e790493771.png

而我们的字符串长度是不可能大于这个值的,所以这里也是会取到结尾。

98a3faa0145e40abaf3c86e735deb1d4.png

这个其实用的不是很多,但这里第一次见,带大家了解一下。

string (const char* s, size_t n)

拿s指向字符串的前n个字符去构造string对象

acb4d65b466540e586fc0c69d2ed35ef.png

string (size_t n, char c)

拿n个字符c去构造string对象

b13147a6d19f434895c97116fa23f796.png

string (const string& str)

拷贝构造:e14e0f10ec4d4705b5f3d122bf010d28.png

2. string类对象的容量操作d2d5388ca33d4af3a1e9ffe2642b5f68.png

总共呢有这么多。

首先我们看到有个size,还有个length:

6a7c3c1860ec4beea8aa6e2f3cb53d1f.png

都是返回字符串长度。

9663ca6b385646389826627fdb307475.png

欸!那他们俩的功能一样,为什么要搞两个呢?搞一个size,搞一个length。

🆗,那这里呢其实跟一些历史原因有关,string呢其实出现的比STL早,string其实严格来说是不属于STL的,它是C++标准库产生的,在STL出现之前就已经在标准库出现了。

那string呢其实最早之前设计的就是length,因为字符串的长度嘛,用length就很合适。但是后面STL出现之后,里面的其它数据结构用的都是size,那为了保持一致,就给string也增加了一个size。

所以size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一

致,一般情况下基本都是用size()。


然后我们看到有一个max_size:

20635f69e8d54d05a05f68c3e1c30b48.png

它的作用呢是返回字符串的最大长度

2f4bf4e9b56e4d3fa7ec1458a4e3c2a2.png

但是呢,真正在实际中字符串可以并不能开这么长,而且在不同平台下这个值也可能不一样。

所以这个东西大家了解一下,知道有这么个东西就行了。

然后我们来看一下capacity

74c4a8e956174a71a3fffda6aa08a560.png

capacity呢其实就是返回当前string对象的容量(即当前给它分配的空间有多大),我们之前学过数据结构,相信这个大家很好理解。

282346bc7a204825af129edb7634744b.png

我们看到这里返回的s的容量是15,但是呢这里想告诉大家这里它是不包含给'\0'的空间的,因为它认为'\0'不是有效字符,所以这里实际上是16个字节的空间。

我们可以调式观察一下:87b1f8eec75a4355978966636276e862.png

然后其它的呢大家可以自己结合文档先了解一下,我们后面再详细一点去解释剩下的一些比较重要的。

3. string类对象的修改操作

61f98c5c62874ca8a36912cfc0ef18b2.png

ps:有的我们放在比较后面一点讲,还有的不重要的我们就简单了解一下。

先来看一下p ush_back

顾名思义p ush_back就是尾插嘛。

971c94aeed0b4cb58dcc9e21b61b25dc.png

演示一下:

2681146d12b444d7ab2ec87788aea97a.png

那这是尾插或者说追加一个字符,那要是想追加一个字符串呢?

也是可以的,不过这里不再用push_back,提供了另一个接口——append

19c2964a98324e4d8a606e5a9258fbf3.png

然后这个append它也是重载了一大堆的版本,但有的其实都不怎么用,所以string的设计其实是被吐槽过的,有些地方设计的不是很好。

那最常用的呢其实还是直接去追加一个字符串:

501ca92894fe4c00ab36666633115232.png

但是呢:

其实平常我们并不喜欢用push_backappend

而是去用:bf0a1fd366c5477587c0f31014cb5922.png

🆗,string还重载了+=,用起来就非常爽:

+=字符,字符串都可以9d6bc3aadd3642ac88e1dd2a3c40c6be.png

不过其实+=的底层也是用的push_backappend,对他们进行了一层封装。

4. resizereserve

那现在我们再回过头来看一下容量中的resizereserve

首先我们来观察一个东西,就是我们定义一个string对象,我们观察一下在不断插入数据的过程中它是如何进行扩容的

那这里已经写好了一个程序:

int main()
{
  string s;
  size_t sz = s.capacity();
  cout << "making s grow:\n";
  cout << "capacity changed: " << sz << '\n';
  for (int i = 0; i < 100; ++i)
  {
    s.push_back('c');
    if (sz != s.capacity())
    {
      sz = s.capacity();
      cout << "capacity changed: " << sz << '\n';
    }
  }
  return 0;
}

我们运行一下:

50a82562225f428aa31ae358479f5ff5.png

我们看到是这样一个情况。

首先我们上面说过了嘛,他这里没有算\0的空间,所以这里看到的是15,实际是16个空间,31实际是32 好吧。

那这样的话我们看到它好像第一次扩容是2倍扩,后面每次都差不多是一个1.5倍扩。但实际呢,想告诉大家,其实在VS上,它这个结构跟我们理解的顺序表还是有一点不同,其实刚开始的数据并没有存到动态开辟的数组上,存到了一个自己的数组里面。我们可以调式观察一下:

7c3c665d247b47d49d32f7f6e1d83e8b.png

我们看到,它是存到这个Buf数组里面了,这个数组的大小是16(不带\0就是15),所以如果string对象的大小16,就存到这个Buf数组上,大于16才存到Ptr指向的动态开辟的数组上,就不往Buf里面存了。

可以认为它的结构是一个类似这样的:

36f0f89cc9844bb3b37180fb5c5f4c40.png

所以如果Buf 满了它第一次在堆上开空间就开32 字节,然后后面是1.5倍增长扩容,所以不能说第一次扩了2倍。

那我们可以验证一下:0b528fc58f3943d0b895015edb5e8671.png

我们打印它的大小发现是28个字节,如果只有指针ptrsizecapacity的话应该是12字节,那现在是28,就是因为它这里还有一个大小16的数组,那这样做的话小块的空间就可以不去堆上开辟了,如果比较大,需要去堆上开辟的话,起始就是32字节,然后不够再扩。

再来看:ff681245c7344a6aa3e039fb14a68889.png

现在i为100的时候,我们看到这时的字符串就存到Ptr指向的动态开辟的空间上了,就不再用Buf存了。

所以我们可以认为string的扩容是1.5倍去扩的,那在所有地方都是这样吗?

不是的,在我们目前的Vs上是这样,不同平台的实现可能就不一样的。

我们可以看一下在Linux的g++下:

94d94ce35192442bbbcd44d2f9f18e04.png

是2倍扩容的,而且这样看的话它是没有Buf数组的。


那这里了解了这个扩容的机制之后:


我们真正想给大家讲的一个东西是什么呢?

🆗,其实是想给大家说一下这个reserve和resize。

那上面我们看到如果我们一直插入数据他是会去不断扩容的,那其实我们是有方法去减少扩容的。

如果我们知道要插入多少数据的话,我们可以去调这样一个接口——reserve,注意不是逆置reverse


228757f6e0b04afa8433c0dc114b8311.png

reserve的作用是什么呢?

c61e24f7cc94436c986d1910bc006814.png

reserve可以帮助我们更改容量大小,这样如果我们知道需要多大的空间,就可以一次开到位,就不用再一次一次的扩容了。

就拿我们上面那个例子来说:

我们现在直接reserve100个容量,但是注意,我们指定100,它不一定开的就是100,可能由于对齐啊等等的一些原因,它会给你多开一些空间,但是肯定不会比100小。

6958a16409024b0e8880e71a1d0467ee.png

这次大家看还有没有扩容啊,是不是就没有了,这里直接开了111,比100多了一些。

Linux下呢:

8ef8aeb6c3bc4f7c85516d01d1d885fa.png

我们看到就是给了100,这就是它们底层实现的机制可能不一样,就有一些差异。

所以呢:

如果我们知道需要多少空间的前提下,reserve就可以帮助我们提前把空间开好,然后就可以减少扩容,提升效率,因为频繁扩容也是需要付出代价的。


那还有一个resize,它的作用是什么呢?


我们说reserve可以去改变容量,帮我们开空间;那resize呢,不仅可以开空间,而且还能对开好的空间进行初始化。

另外大家要知道reserve只是开空间改变容量,它是不会改变size的:

73142544023e41a386e16a9faac6c770.png

resize呢:

09e56dd493c84d5eab5ec0975df53ead.png

7744c1d78d1f42c48b919e8d6833f096.png

我们看到capacitysize都变了,因为它是会对开好的空间进行初始化的,相当于插入了新字符,所以size也变了

126ea9de1951484b93d9dc39b27ea163.png

这里我们没有指定第二个参数,既要填入的字符,默认给的是\0,当然我们也可以自己指定要填入的字符:

5ef12c3e02724311a308d99d4e510c07.png

当然我们刚才传的第一个参数n是大于当前字符串长度的,那么他就去扩容,如果我们传的n小于当前字符串长度,它还可以帮我们删除多出来的内容:

222e9ad3f45742989611f17e0668c743.png

那大家思考一下,这样做的话,会改变capacity吗?

eded654ea7d3480ca0c9f2e38acf0546.png

我们看到只是size变了,capacity并没有改变。

因为一般情况下是不会轻易缩容的,缩容的话一般是不支持原地缩的,我们之前学习realloc扩容有原地扩和异地扩两种方式,而且原地扩也是有条件的,后面要有足够的空间才能原地扩。

而缩容呢?可以原地缩吗?

🆗,由于底层内存管理的一些原因,是没法原地缩的。

如果支持原地缩,是不是就要支持释放一部分,我们申请一块空间,不用了只释放其中的一部分。

但是是不支持只释放一部分的,就像我们free是不是要求传的指针必须是指向其实位置的。

所以如果真的要缩容的话,只能异地缩,就是开一块新的小空间,把需要的数据拷贝过去,然后把原空间释放掉。所以缩容是要付出性能的代价的,系统原生是不支持的,我们需要自己去搞。所以不到万不得已不要轻易缩容。

不过其实string是提供了一个可以缩容的接口的——shrink_to_fit

10d9e82134854a409b7614d8dfb8230c.png

5ef67101de5242d0b22940e6dc54f2eb.png


但是它是要付出代价的,所以我们要谨慎使用。

5. 认识迭代器(正向)

那现在大家思考一个问题,如果我们想遍历一个string对象,可以有哪些方式?

首先可以循环用[ ]遍历,因为string是重载了[ ]的,或者我也可以用范围for。

那除了这些方法之外呢,我们还可以用迭代器。

67e323935c094866a07598d38d698b4e.png

举个栗子:

int main()
{
  string s1("hello world");
  string::iterator it = s1.begin();
  while (it != s1.end())
  {
    cout << *it << " ";
    it++;
  }
  return 0;
}

18b61f0bdb6843a98f4a8f5ace132070.png

解释一下:首先这里的it就是我们定义的一个string类的迭代器(string::iterator是类型),那这么理解迭代器这个东西呢?

🆗,现阶段呢,大家可以认为它是一个像指针一样的东西(不一定是指针)

06f197bdc193477fbb76c4548b7c4ab8.png

那这里的begin呢,会返回指向字符串第一个字符的迭代器。

7f70f3ce2fe946659593c4cada4265d5.png

afb54826d5b641f48f934931c4ec9dc6.png

end返回指向最后一个字符后面位置的迭代器。

我们就可以理解成指向这个位置的指针:

7ef3faf696dd4be389eb34d124476bc5.png

那这样我们去循环遍历,解引用it,就可以遍历到整个string对象。

那这样对比一下的话是不是用范围for会比较爽一些:

242304f01ca84ecb9af6881472908494.png

🆗,那这里想告诉大家的是范围for看起来好像很牛逼,但是其实它的底层也是用的迭代器。

6. 反向迭代器

那迭代器除了像上面那样支持正向从前向后遍历,其实还可以倒着遍历,倒着遍历的叫做反向迭代器

16f5f03ce0494e0c8cf66efc9cd344a0.png

我们看到除了begin和end这里还有rbegin和rend,它们返回的是reverse iterator 即反向迭代器。

rbegin和rend返回的是什么呢?

0f26e661d5e04d04bd08d7f184fb64e0.png


98feed597deb49808da0142de30c9a2b.png

string s1("hello world");那还拿这个对象举例子,

大家就可以理解为rbegin是指向字符d的(但实际实现不一定是这样),rend是指向字符h的前一个

那我们来用一下:

522f8905e59640fc87ca43d64e805d93.png

那大家先思考一下,这个地方rit应该++还是- -?

🆗,还是++,大家可能认为这里从后往前倒着走应该是- -了。不要这样理解。

大家想,正向迭代器++是往后走,那反向迭代器就是方向相反了,那++不就是往前走了嘛。

我们验证一下:37ed9d6adc334fbf83a682765f8773df.png

是不是就反向遍历了。

7. const迭代器(正向&反向)

那大家再来看这样一个场景:


4089c2cd88da458b99515d00ae08be1a.png

我们把s1传给一个函数,然后在函数里面用迭代器遍历打印它,但是这里报错了:

53551ceb3eeb436fbbd2e47b2dca34a6.png

说不存在什么到什么的适当转换。

为什么呢?

我们看到函数func的形参s是s1的引用,但是加了const修饰,也就是说,与我们上面写的代码的区别在于这里的string对象即func中的s是const对象。

那s是const对象为什么这里就不行了呢?

🆗,const对象是不是就不能被修改了啊,那我们上面讲普通迭代器的时候说了,可以认为它是一个像指针一样的东西,那我们对它解引用是不是就可以修改它了,所以这里我们就不能用普通迭代器了,这样是不是就权限放大了,所以这里才报错了。

那怎么解决呢?

ddaad53db30645ab914c53f0620ecac7.png

🆗,我们看到begin是有两个版本的,如果是const对象调用begin,那么返回的是const迭代器const_iterator
普通迭代器可以读容器的数据,也可以去修改,但是const迭代器就只能读,不能修改。

ce010c8592da4e34bf7d6fb9edcc5b0f.png

所以这里s调用begin返回的是const迭代器,我们用const迭代器迭代器接收就行了。

97cf9a21511041f7b42ba090cf3acacc.png

当然const迭代器我们是不能去修改的(不能修改它指向的内容,其本身可以修改)。


那同样的道理:


普通的迭代器有正向和反向,那const迭代器就也有正向和反向的两个版本。

刚才我们上面的就是正向,即const对象调用begin和end返回的迭代器。

那const反向迭代器就是const对象调用rbegin和rend返回的迭代器——const_reverse_iterator

f76d2e68db8a46128fd360c7c499512b.png

104e0e6c411c426fbf3a6e265414c1d4.png

我们来试一下:

0f63ab450c49408d8e1babf6603466a0.png

当然这里我们看到迭代器的类型是不是有点长啊,那我们可不可以简化一下呢?

是不是可以用auto啊:

ed671d04b7984abdb3c57acd6e902622.png

我们说auto是不是可以自动推导类型啊,但是如果你明确知道这里返回的是什么类型,写成auto可以简便一点,那如果给不知道的人看可能会有点懵。

所以说它并不是一个完全完美的东西。

那还要给大家提一下的就是:

f1c5bce4766d4a64a081ec85eb321495.png

我们看到这里C++11又提供了一套迭代器:cbegin cend crbegin crend,它们只返回const迭代器。

为什么搞出这些呢?

它是这样想的,我们上面讲的迭代器,比如都是调用begin,普通对象调用返回普通迭代器,const对象调用返回const迭代器,好像有点不清楚,它期望你普通对象就去调上面我们讲的不带c的那一套,const对象就调带c的那一套,规范一点。

但是呢,好像不是很必要,而且大家一般也不太喜欢用这些新的,所以这个大家了解一下就行了。


8. 元素访问

870d5b8bda6b4d948dfbad228c839241.png

那首先呢就是[],string是重载了[]的,我们可以直接用:



47c5697db0544fd4998d13d8e303c637.png

然后这里想跟大家说的是:

f6b766d38de4469091510a21e278daee.png

operator[]也是有普通版本和const版本的,普通对象调[]就返回char&,可以去修改它,const对象就返回const char&,不能修改。

然后我们看到还有一个at

at作用跟[]是一样的,而且它同样也有const和非const版本:

88f5dc46f9614689909e18195a63b32b.png

但是呢,它们两个还是有区别的,区别在于:

[]如果越界访问的话是直接报错的,它内部是断言去判断的

1a31fd6354e642b4a902eb759a4b87c1.png

但是at

2166257c65d74590999e946b8bc50314.png

我们看到是这样的,它其实是抛了个异常

89e1882a6d6a4ed8b7e05ae0dfc66f43.png

异常是可以捕获的,但是现在我们还没学,大家先了解一些就行了。

但是在实际当中其实我们很少会用at

然后还有一个backfront

其实就是返回最后一个和第一个字符,但是这个我们用[]就能搞定,所以大家简单了解一下就行了。

9. insert和erase

那到这里我们在回过头来看几个之前跳过的内容。


先来看一下insert:


借助insert我们可以像string对象中插入字符和字符串

1d68d68209674b0290db5ac3c0c82896.png

我们看到这里也是提供了好多版本,但是有的并不常用。

那我们来练习几个比较常用一点的。e3878841b2664ef4ab066c95176e3083.png


现在有一个string对象s,那我们现在想在world前面插入一个字符串hello,怎么搞?

那我们就可以考虑用这个:

f639b6ddd0bf402795baf075b56c912e.png

第一个参数指定我们要从哪个位置开始插入,第二个参数指定要插入的字符串:

f22c34d426654d1ca51ab1909edfac09.png

然后我们又想在hello后面插入一个空格,怎么搞?

首先可以这样:025c337bd06342f5a2e1174eafcda310.png


或者我们可以去调这个:7c95745d95ed4f4daba5f831239fe8cb.pngb4a54a4c77744020a4b6aac1f0679c84.png

除此之外还有这个接口:

6d351183798c4588bc094a05bad9ca48.png

我们看到这个不是传下标,而是传目标位置的迭代器:

392b21900f0d422ea5bde8ea08191e15.png

这样就可以了。🆗,那然后大家思考一个问题:


对于string来说,大家觉得可以频繁使用insert吗?或者说经常用insert好不好?

那我们其实是不推荐经常使用insert的,为什么呢?

我们说了string底层是字符数组,那我们学过数据结构知道在顺序表里插入元素是不是要挪动数据啊,效率是比较低的,所以呢insert我们能少用就少用。


于insert对应,接下来我们看一下erase:


那erase呢其实就是去删除string对象里的元素。

b5f8312de3104e53b42d0f50f2ed333e.png

举个栗子:

53dffba08e944f60b26d8885e7e44226.png

现在我们想删除s里面hello后面的空格,怎么搞?

67b3920690aa42de96793352391e762e.png

可以用这个,从第5个位置开始删除一个字符:03a22bb642624e678edf440ccbd9ff91.png然后我们看到这里len的缺省值是npos,npos我们之前是不是说过啊,在这里是整型最大值,所以这里如果我们传的len比字符串长度大,或者不传,它都会删除字符串结尾。

8636ba943afd440daea2be9a7c52a971.png

f9869a395d83493996cd4b61368b0c5a.png

还可以用这个:

c05b114c8ab14e23bb427b8970ded912.png

传迭代器的位置

d21abfd84ed942de9e2015fc2166db15.png

然后还有一个接口涉及到迭代器区间,我们这里先不说。

那同样的道理,对于erase来说,如果我们只删除中间的一部分,是不是也要挪动数据啊,所以,erase也不推荐频繁使用。

10. replace、find、rfind、substr

我们再来了解一下replace:42bb6d8775d044e48ef6d495faf8a9e8.png

replace其实就是可以把字符串中的一部分替换成新内容。

也有很多版本,我们不可能全部都讲,必要时大家可以自己查阅文档。

我们看这样一个场景

1ff0155db16940ad921327e5bdd47008.png

现在想把s里面的空格替换成"%%d",怎么搞?

2a356ad6603946eb86349dcccbfbe2c0.png

4077b57636d943cb899b3730ae86dae1.png

确实完成了,但是大家看,这样做真的好吗?

有什么问题?

首先可能会需要挪动数据,我们当前这个例子就有,其次,空间如果不够还得扩容,所以这个东西我们也尽量避免去用。

然后我们再来看一下find:


f13b863edf714e0b812c063f074388bc.png

find可以在字符串里查找字串或者字符,返回对应的下标。

找不到返回npos

举个栗子:

fef0f637aa654576ad863c0d36cc7fb3.png

在s里查找空格,怎么搞?

b742800bf2cf4e6eaf00bb72939baad0.png

当然也可以查找字符串:

1218a79310c942bb9771698c22b55d89.png

然后我们还看到:


它还给了一个缺省参数pos,缺省值是0,其实pos呢是用来指定我们开始查找的位置的,我们自己不指定那就默认从0开始,我们指定了,就从指定的位置开始找。

我们可以试一下:

a426b121f52f42ba89ca125329bf9c38.png大家看这个,我们要找ll,但是我们指定从下标5的位置开始找,那是不是就找不到了,所以返回npos,打印出来的就是npos对应的值。


那我们来看这样一个问题:


假设现在有一个文件名的字符串,比如说string.cpp,我们想把后缀名取出来,怎么搞?

这时我们就可以考虑使用substr这个成员函数。

cd27680b21374e83865aeb06c2028f99.png

它的作用是什么呢?

它可以帮助我们获取string对象中指定的一个子串。

参数:1887049742db4d94934a1424a8c15901.png

那有了substr,我们就可以怎么解决上面的问题:

我们是不是可以先用find找的.的位置,然后计算出后缀名的长度len,从.的位置开始,向后获取长度为len的字串

int main()
{
  string s("string.cpp");
  size_t pos = s.find('.');
  if (pos != string::npos)
  {
    string suf = s.substr(pos, s.size() - pos);
    cout << suf << endl;
  }
  return 0;
}

运行测试一下:

9bfea9a215ea4ab19a2f58b62cedcc3a.png

这不就拿到了嘛。

那再看,如果是这种情况呢?

07f2ca8869764a10ad6c897b04f4c0ca.png

它有多个后缀,但是我们只取最后一个,这下应该怎么办?

🆗,那除了我们上面学过的find,还有rfind

13e1950e468d4d7a84c35b5d97ebb83f.png

那rfind和find有什么区别呢?

区别在于find是从前往后找第一个匹配项,而rfind是从后往前找倒数第一个匹配项。

所以当前这种情况,我们是不是把find换成 rfind就行了啊:

859c160ea17349f687541fa729cbb593.png

当然其实刚才这两个场景我们使用substr可以不传第二个参数的

263a18f827dd4d7d9e2eec8d7c3109e7.png

这里给了缺省值npos,也就是我们不传的话,它默认从我们给的位置一直取到结尾。

6b3f5c223a5846e68b969cdd69631db0.png然后呢:

我们看到substr后面还有一个compare,就是进行string对象之间的比较嘛,但是这个compare我们一般用不上。

因为string还重载了关系运算符


ac32fb43649c450db0fd339c7766ee32.png

我们可以直接用来比较

但是这里我们看到光是==它就重载了3个版本,但是其实根本没必要,所以string类的设计其实是被吐槽过的:

7255d1b34d09444786b9600bf4e54386.png

11. swap与string::swap

string里面呢,还有一个成员函数叫做swap:

568115635eef476e97c316c242a42309.png

cfe6c2562c504a87b7289b39b84363a3.png

它可以接收一个string对象,与当前对象进行交换。

演示一下:5dc6ab51e12640e9873d78915576a9f1.png

🆗,那除了这个swap之外,我们之前不是提到过说C++的库里面也有一个swap,它是一个模板函数。

所以我们也可以这样交换两个string对象:

b814ce1707344c4eb49255514ee3dfbc.png

🆗,也可以完成交换。


那大家来思考一下,这两个swap哪一个效率更高一点?


🆗,那这里肯定是string的swap是更高效的一点的,因为string::swap作为string的成员函数,那它里面想交换这两个对象,就可以怎么办,是不是可以直接改变指针的指向(交换它们两个的成员变量)啊:

28e46bcf3fb741f398a7e1d0b6118663.png

而库里面的这个swap是怎么交换的:

1eea12f2633d4e4a8a671cfafb6bba43.png

🆗,我们看到这里是构造一个临时变量,然后又有两个赋值,而string的拷贝是需要深拷贝的,所以它这里是比较低效的。

那这里我们先简单的说一下结论,后面我们模拟实现的时候会对这里有一个更深的理解。


12. c_str

下面我们再来看一个string的成员函数叫做c_str:

8af64a89c5ec4f97aa13436a9aedc771.png

那它的作用是什么呢?

65a776e5c8cb41a8b37a53b0c144afb5.png

它其实是去返回一个指向当前string对象对应的字符数组的指针,类型为const char*

所以,如果我们想打印一个string对象,就可以有这样两种方式

69c923d495b0449781937b2802f8fd1f.png

你可以认为c_str返回的指针就类似于这里的指针p

bce7d0e5378a4bf49459a0cf72975204.png

ef01ca6e368e4ce28bf52d2a33788156.png

那这里第一个打印其实调的是string重载的<<

ea7e8442ccb449a88d7f6db9b89b8fbe.png

第二个调的就是库里面的那个。

那上面那个例子我们看到两种方式打印出来没什么区别,那如果这样呢?

int main()
{
  string s1("hello world");
  s1 += '\0';
  s1 += '\0';
  s1 += "*******";
  cout << s1 << endl;
  cout << s1.c_str() << endl;
  return 0;
}

这时我们再用这两种方式打印:

fe1006b234d14c958f7270e136bdb9be.png

我们看到结果就有区别了。

为什么呢?

因为第一种方式我们直接打印string对象s1,它是去看s1对应的size 的,size是多大,总共有多少字符,全部打印完。

但是我们第二种打印c_str返回的const char*的指针,它是遇到'\0'就停止了。所以大家可以理解成c_str就是返回C格式字符串。

e4ff19b086e8422f9d97dded7c78acb9.png

13. getline

我们来看这样一个场景:

int main()
{
  string s;
  cin >> s;
  cout << s << endl;
  return 0;
}

这里定义了一个string对象,现在我们要自己给它输入一个值,然后打印输出。

我们来试一下2d1adcc60ee6493287bee105952fde7a.png

嗯???但是我们发现这里出现了一点问题。

我们输入的是hello world,但是为什么打印s出来只有hello啊,怎么回事?

🆗,大家要知道,C语言里的scanf,包括这里的cin,我们在用它们输入的时候是不是有可能输入多个值啊,那当我们输入多个值的时候,它们默认是以空格或者换行来区分我们输入的多个值的。

所以我们这里输入的hello world,会被认为是两个值以空格分隔开了,所以cin值读到了空格前面的hello,后面的world就被留在缓冲区了。


那这种情况怎么解决呢?


🆗,我们就可以用一个函数叫做——getline

604ce09090804eda8bac28a42c85926b.png

ffaf7578425d43f19f8b5a15d531fba6.png

getline呢它读取到空格才结束,当然它还支持我们自己指定结束符。

第一个参数就是接收cin,第二个参数接收我们要输入的string对象。

我们试一下:

5b7aff17e72941e58f3eeb1cb2b9ba39.png

🆗,这下就可以了。


2.4 总结

那我们简单总结一下:


我们这篇文章关于string使用的讲解差不多就到这里了,string提供的接口是比较多的,我们不可能全部都讲完,当然其中大部分我们平时可能都不会怎么用到,常用的其实是比较少的,有些没讲到的后续如果大家有需要,可以查阅文档进行学习——链接: link


那我们这篇文章就到这里,欢迎大家指正!!!

后续我们还会对string进行模拟实现,到时候有些地方我们或许能够理解的更深刻一点。

e9f574b56b1b477381e6ab5a6f15da7a.png


目录
相关文章
|
1天前
|
存储 算法 程序员
【C++进阶】深入STL之 栈与队列:数据结构探索之旅
【C++进阶】深入STL之 栈与队列:数据结构探索之旅
|
1天前
|
存储 缓存 编译器
【C++进阶】深入STL之list:模拟实现深入理解List与迭代器
【C++进阶】深入STL之list:模拟实现深入理解List与迭代器
|
1天前
|
C++ 容器
【C++进阶】深入STL之list:高效双向链表的使用技巧
【C++进阶】深入STL之list:高效双向链表的使用技巧
|
1天前
|
编译器 C++ 容器
【C++进阶】深入STL之vector:深入研究迭代器失效及拷贝问题
【C++进阶】深入STL之vector:深入研究迭代器失效及拷贝问题
|
1天前
|
存储 算法 程序员
【C++进阶】深入STL之vector:构建高效C++程序的基石
【C++进阶】深入STL之vector:构建高效C++程序的基石
|
1天前
|
编译器 C++
【C++进阶】深入STL之string:模拟实现走进C++字符串的世界
【C++进阶】深入STL之string:模拟实现走进C++字符串的世界
|
2天前
|
编译器 数据安全/隐私保护 C++
c++primer plus 6 读书笔记 第十三章 类继承
c++primer plus 6 读书笔记 第十三章 类继承
|
1天前
|
编译器 C++
【C++初阶】—— 类和对象 (下)
【C++初阶】—— 类和对象 (下)
|
1天前
|
存储 编译器 C++
【C++初阶】—— 类和对象 (中)
【C++初阶】—— 类和对象 (中)
|
1天前
|
存储 编译器 C语言
【C++初阶】—— 类和对象 (上)
【C++初阶】—— 类和对象 (上)