【c++】:STL模板中string的使用

简介: 【c++】:STL模板中string的使用

STL简介



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


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的六大组件

仿函数,空间配置器,算法,容器,迭代器,配接器。


STL的缺陷

1. STL库的更新太慢了。这个得严重吐槽,上一版靠谱是C++98,中间的C++03基本一些修订。C++11出来已经相隔了13年,STL才进一步更新。

2. STL现在都没有支持线程安全。并发环境下需要我们自己加锁。且锁的粒度是比较大的。

3. STL极度的追求效率,导致内部比较复杂。比如类型萃取,迭代器萃取。

4. STL的使用会有代码膨胀的问题,比如使用vector/vector/vector这样会生成多份代码,当然这是模板语法本身导致的。


一、认识string


7d0c508d98934debb559a78bd5e790e4.png


我们发现wstring,u16string,u32string这些又是什么呢?string的本质是一个管理字符的顺序表只不过里面存的都是1个char类型的字符,而wstring里面存的是2字节的char,u16string也是2个字节,u32string是4个字节,为什么会有这么多的差异呢?因为我们有管理不同的字符数组的需求。在这里我们要了解ascll码,用ascll编码可以在计算机里面存储和显示英文信息,而在一开始的ascll码表中仅有128个值,用7个比特位就可以代表这128个值了,所以一开始的string中的字符仅为1个字节,如下图:

2885299c69f7471584e25fa39fdb25f3.png


apple这个字符串存在char类型的数组中会消耗6个字节,多出来的1个字节用来存放\0,而字符a在ascll表中的值为97,内存中的16进制61转换过来就是97,所以字符确实是根据ascll的值在内存中一个一个存储的。而计算机不能只显示英文,如果只能显示英文计算机又如何卖到中国呢,所以为了显示其他国家的文字有人就发明了Unicode(万国码)能表示各个国家的文字,Unicode又分为utf-8,utf-16,uft-32,他们三个的区别是每个字符的字节数不同,比如utf-8的char就是1字节并且兼容ascll吗,utf-16用16个比特位也就是2个字节,utf-32则是4个字节表示一个字符。而string类就是因为这样的原因所以搞出了字节数不一样的模板。


总结:


1. 字符串是表示字符序列的类

2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。

3. string类是使用char即作为它的字符类型,使用它的默认char_traits和分配器类型。

4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数。

5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

总结:

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

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

3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string;

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

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


二、string中基本功能的使用。



1.string的构造函数。

0a412adceb394dd3932c129d4521fd74.png

我们可以看到string的构造函数有7个不同的重载,第一个可以直接定义一个字符串,比如:

885ebd4ae00c4c958754eaf203eaffd9.png

我们可以发现一个空字符串里面是有一个\0的。

第二个可以直接用字符串初始化:

a5f4cd4183ad48ee848e5afd1a0ae678.png

第二个是我们使用最多也是最方便的。第三个:给定一个字符串从这个字符串的某个位置及这个位置后面的len个长度初始化,如下:

e9945bbe35f3432fa10646fa29cff4ea.png

len这个参数给了缺省值,实际上如果我们不给len那么len默认就是npos,npos是size_t类型默认值是-1,我们都知道size_t类型是无符号整形,所以-1就是整形的最大值,也就是说如果你不写这个参数那么自动将从pos位置开始后的所有字符进行构造。

707bb3228be14b7680ef8aecd444bf9b.png

40a94932423249b4a4b56e34cd8e112a.png

第四个:直接用字符串去构造。

ebf8768dd69042499d70f75b036e6a99.png

这里发生了隐式类型转换,将const char* 转换为string,如果不想要编译器发生隐式类型转换我们可以在构造函数前面加上explicit,这点我们在前面的类和对象文章讲过。


第五个:给定一个字符串用它的前n个构造

2f7d9e00c2604baabda1dcd15be58552.png

第六个:用n个字符去构造

781e8ed8133447bd9ec1944dbd4f5678.png

第七个:迭代器区间的构造:

8084085f02554c0ebb7276ff53ee7bb1.png


下面我们先来看string容量部分的函数:4f0b1ba99a814864a0601e07df03f2f3.png

第一个:size代表字符串的长度,length与size一模一样。

d556db613be244cdbf87ce77dffd02ff.png


那么为什么要设计一模一样的函数呢,这里是因为c++早期的历史遗留问题,在没有STL之前是用length计算长度,有了STL后要计算二叉树等再用长度这个名称就不合适了所以多加了一个函数size。


第二个:max_size

84325b7500da4f4b872a1e20d4437b8b.png


max_size就是字符串的最大长度,它的理想是整形的最大值但实际上没有这么大因为要看堆的大小,并且这个借口并没有什么很大的作用。

我们先讲第四个:capacity

f5eed205d7dd455eb4c164e375a4f203.png

capacity就是字符能存储多少个字节,在这里需要注意的是:capacity不包含\0也就是说如果字符串是:“hello”,那么capacity就是5不会再加上\0的一个字节大小。


第三个:resize

23e1fe221c86473ab20af9bd2ca19ebe.png

第一个作用:resize的作用是开空间并且初始化,并且resize会改变size和capacity的大小。

8362bc81e41a401aa2b06188bdadc119.png

3463ffe740bd4f9b89e4eeef3575d9a9.png

我们可以看到,s1的长度从11变成了50,空间从15变成了63,当然如果我们不主动初始化为某个字符会默认初始化为\0。

4e3b8529775b42ce86a3bc2dc0906436.png

第二个作用:当resize的大小比原来字符串的capacity要小,那么resize就会将字符串中的字符缩减为resize的大小。

2f735f6fcdc34f9eb7c07c9ea12661f8.png

我们可以看到字符串只保留了前五个字符。

第五个:reserve  开空间,只改变capacity不改变size,并且不会初始化。

7dac6ba0cc38470ca8adc673f1cd7be8.png

第六个:clear  清空字符串

dd4cc36670b446deaccad22874a437bd.png可以看到字符串为空了。

第七个:empty  判断字符串是否为空

9080715c10944e5797a365c5dd302997.png

接下来我们看string中的modify接口:

b8dbb5a6957c4b9f8fd1af73c42fc055.png

这里我们就不按照顺序进行演示了,因为有些接口不按照顺序效果会更好。

push_back  :尾插一个字符

931c15ed31b8455998f92f39f1d25717.png

append:尾插一个字符串

d325a35b788a4d5abb84a80e3d665e6c.png

第3个重载:

e1216ff4127a44ca85e003088c2a3a60.png

这里也是有6个函数重载,实在太过冗余所以我们就演示经常使用的。


第2个重载,尾插一个字符串的从pos位置起的sublen个字符


a3836036663a4b4ca9c53672a6997706.png


第4个重载,尾插一个字符串的前n个


40b4d72bb91a4d9a812937676f2b5c8e.png


第1个重载:


当然以上的这些接口其实都不是很实用,最实用的是操作符重载中的+=符号。


660839e7754f463a955bf2ca78522c33.png


可以看到+=符号实在是太方便了。

496a9548cf8345d1bcfc9a401e93de54.png

insert:插入字符串或者字符

239ddff20f2d4f50922888720512be05.png

下面演示一下如何使用:在第pos个位置插入一个string对象

6a5769391c5d4df6b2fcabcdd963c3c5.png


在第pos个位置插入一个string对象的从subpos下标位置开始的sublen个字符


ec976bb293994ca99b9cc98d3db7044f.png


在第pos个位置插入一个字符串

bfbb69f0aabb4ea4a1bfd3be68bfa052.png

在第pos个位置插入字符串的前n个。

f0a6c7cf791942cba5e7f68b7280f92e.png


当然我们是不推荐使用insert的,因为插入要往后挪数据,时间复杂度为O(N)。


有insert就会有erase,我们看一下erase接口:


73b332ead607414490bb56652e18bec7.png


从pos位置起删除len个字符:

cf42a82a4bdc4652a7b5c0c8161ca50c.png

len有缺省值,如果我们不传则从pos位置开始后面全删,如下:


b1f9c500a704456ebd5c12f15cc7ff42.png


接下来我们看一下replace接口:

c39e46528a51433da8ae8b9063a819da.png

replace的重载太多了并且冗余,我们会用一两个即可:


3d50d965ab59452fa471dd3a1260bffd.png


上图是从下标为pos的位置开始的len个字符串。


我们先一下find接口:


852adc143d554e828502402cdb63fba1.png


pos使用缺省值如果我们不写默认是0位置。

d457c25fe5f04f92aeb81bd6f695226e.png

下面我们用find接口和replace接口做一道经典例题:将空格替换为%20


9cacef48108b44ae8d62608dbd7d8167.png


看到以上的代码我们还能在优化一下吗?答案是可以,我们每次查找完一个空格就没必要再重头开始查找了,我们从上一次的位置+3开始查找即可,为什么要+3不是+1呢?因为我们替换了%20是3个字符:


1eab5d8b506a49f899b121a0f6ca639b.png


那么我们还能继续优化吗?大家还记得我们讲的reserve函数吗,我们可以提前开好空间避免在替换字符的过程中持续开空间浪费时间。


1eab5d8b506a49f899b121a0f6ca639b.png


我们用另一种方法再做一次:

171be13d5eb54440979f5b4963c06b21.png

这次的效率很明显是高于上面那种方式的,当然这样的方式提前开好空间效率也会提升不少。


下面我们来看一下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;
}

f9da607b45ba45d9865a6e0eeda45ade.png


我们可以看到在vs下是按照1.5倍进行扩容的。


接下来我们看一下一个字符串的大小:


59926004ec5846d994d4e6ded50d0706.png


为什么一个空字符串的大小为28呢?看下图:

223f5ffd66df4c8da411a178d82e9853.png

在VS下string的底层是这样实现的,当字符串很小的时候就不用频繁的开空间了直接用数组即可,当字符串很大就需要用_str开空间,所以大小为28,下面我们看看linux下的:

0c343ea9f04143e39db927e10848dcc7.png


同样的代码我们运行起来:


2b1e9469063e405ab4b5bec0e9a6f1b1.png


我们发现在linux下是严格按照2倍扩容进行的,而且字符串大小为8,那么为什么linux下没有size和capacity变量呢?因为linux下的string是按照写时拷贝实现的,string对象内部只有一个指针,8字节是因为在64位地址下,该指针将来指向一块堆空间。


97ec132d4162486885a20c98e9d10dae.png


string迭代器的使用:


f6df5e898c2f41efb519194216126ed2.pngf12aff8b3abb4335a2ecb0d965ee5934.png


要注意的是迭代器的区间是左闭右开的如下图:

0cc76b2b12fa4cb2b014a8eee01b20bf.png



把begin给it这里的it就相当于指针,只有解引用后才是指针指向的内容。而我们最喜欢使用的语法糖实际上就是用迭代器实现的,并且这个实现非常的简单,类似于宏替换。


073988f84bf74c698156af6bbe944ac9.png

0b9bfeeabbf54d71b322f9ffa6c26885.png


通过汇编我们也可以看到范围for实际上去调用迭代器的begin和end函数了。


接下来我们看反向迭代器的使用:

472266d30f1748a4acba8d43eaf35a26.png

反向迭代器只需要在前面加上reverse即可,相对应的begin和end前面也加上r,需要注意的是反向迭代器原来的反方向,还是++向前走。


那么像下面这种情况该怎么办呢?


2a5775771ec545319a7826741fcdb0dc.png


这种情况下我们就不能调普通迭代器了我们需要调用const迭代器。如下:

ad5c531d58664f899ac94462c84139d8.png

那么为什么存在const迭代器呢,因为我们有时候是不希望别人修改我们的代码的。


4ecd33f14ae24908ad21b8ba38dab9e8.png


正常的迭代器允许我们去修改,但是const不行。

3a213028b12648759f03f5fe3aea34ca.png

下面我们看一下string中用于访问的两个接口:

12906adc94ba4345a4ba15ff00ad4863.png

8ebd1d8b47664a82af30006f5a1977ca.png


那么这两个接口有什么区别呢?

2bccb28a7076472c80de8ed1338c403e.png

032747ea4a8a4f3e9c81d1d53d379eda.png


955ebe13a5ed4f249e64b1ec8124a462.png


我们可以看到当用【】越界时会直接报错直接终止,而at则是抛异常。


下面我们来看一下swap这个接口:

a2a36cee5abd49ff89b585a9b04ea6c9.png0e2621a5206b4317b1ac785080e7814d.pngc15e1f2180d34c5493bc982fea9e3967.png




string中的swap与std::swap是不一样的,string中的swap是直接换指针的指向,而std::swap则需要调用三次拷贝构造函数,所以string中的swap的效率是更高的。


下面演示一下c_str这个接口:


b4ee49b6672742cf85df8f405b0391ec.png


不知道大家是不是会有疑问,c_str和直接打印有什么区别呢?看下图:

786a1a5d36e0431097881e248545d277.png

c_str是按字符串进行打印的,也就是说遇到\0就停止。而直接打印是按照字节数也就是size去打印,不去管\0.


接下来看substr这个接口:

74eb7ad8c6e548b89d87108330e0fd1d.png


substr是取从pos位置开始的len个字符的子串。find函数我们前面讲过,其实还有一个rfind函数,find函数是从前往后找,rfind函数是从后往前找。

3dc89c5c01f94d14824dabffac9382c7.png

482edc1662de47fca332f10376b2ed71.png


可以看到我们成功取到了文件的后缀。


find_first_of:  从前往后查找任意一个出现在字符串内的字符


d4e1fa982c184786a688795b8c83c3d4.png


可以看到我们成功将abcd全部替换为*,还有一个与之对应的函数find_first_not_of,这个接口的作用是找到不在字符串中任意一个字符的位置。如下图所示:


23cd3ad2fdfd4d30bc9bfc11956d8d0b.png


可以看到这个接口与find_first_of是相反的。


find_last_of: 从后往前查找任意一个出现在字符串内的字符

0fe0f3bb1ed64d82a0acd80288044679.png


同样还有一个find_last_not_of接口是与find_last_of是相反的:


d9b7869a9b5a497ab7411fa33d4cc513.png


下面我们说一下字符串比较的几个接口,这里的比较与C语言一样都是通过比较字符的ascll码值来确定大小的,string中的接口compare是不经常用的,我们直接看运算符重载:

51df83aa6d17441594452c3d7932c325.png


这里又再次说明了string中的接口是在太冗余了,光是相等这个重载就有三个,实在是不得不让人去吐槽。下面我们看怎么使用:

665a16b6add64887b4339e217aecc578.png

也就是说我们可以直接对象与对象进行比较,对象与字符串进行比较,字符串与对象进行比较。后面的其他符号都与等于符号同理就不在一一演示了。


下面我们讲一下getline这个接口,这个接口在写字符串类型的题目的时候非常有用:

609e7a54378b489db95dbfe3ef01aef3.png


getline就是再我们用cin输入字符串的时候以换行作为结束标志,遇到空格不会结束。为什么要说这个问题呢,因为我们发现在做题的时候当输入一个字符串字符串中有多个空格的时候只会输出空格前的字符,而空格会被留在缓冲区。getline的第一个参数为cin,第二个参数为字符串。如下题:

7994fc1029e24ee2ac29b6bc598108b1.png


不通过的原因就是cin只识别了空格前面的字符,下面我们用getline来试一下:

1f16b83ca7d54e25b0ec7c36d93bf9d1.png

看来getline成功解决了我们的问题,所以在遇到这样输入带多个空格的字符串要用getline才可以。看到这里我们就已经把string的常用接口都演示了一遍,string一共有一百多个接口而其实大部分都是冗余的平时用不上的接口,大家就只需要把这个常用的接口用熟就能很好地使用string了。


总结



string中的重要接口有以下几个:size()  reserve()   resize()  operator+=    c_str    find + npos   以及比较操作符的重载,当然即使会使用这些接口也很可能对接口云里雾里,下一篇我们用c++模拟实现一个string类,届时大家可以更深刻的了解string。


目录
相关文章
|
1月前
|
C++ 容器
|
1月前
|
存储 程序员 C++
C++常用基础知识—STL库(2)
C++常用基础知识—STL库(2)
71 5
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
22 1
|
1月前
|
编译器 程序员 C++
【C++打怪之路Lv7】-- 模板初阶
【C++打怪之路Lv7】-- 模板初阶
18 1
|
1月前
|
存储 自然语言处理 程序员
C++常用基础知识—STL库(1)
C++常用基础知识—STL库(1)
58 1
|
1月前
|
C++ 容器
|
1月前
|
C++ 容器
|
1月前
|
存储 C++ 容器
|
1月前
|
算法 安全 Linux
【C++STL简介】——我与C++的不解之缘(八)
【C++STL简介】——我与C++的不解之缘(八)
|
1月前
|
算法 数据处理 C++
c++ STL划分算法;partition()、partition_copy()、stable_partition()、partition_point()详解
这些算法是C++ STL中处理和组织数据的强大工具,能够高效地实现复杂的数据处理逻辑。理解它们的差异和应用场景,将有助于编写更加高效和清晰的C++代码。
25 0