【C++】STL:string
这是接触 STL 的第一篇博客,让我们以 string
为始,走入 SLT 的世界吧!
[TOC]
1. 何为 STL
STL 是 C++ 标准库的重要组成部分,其作用是为绝大多数数据结构提供轮子,是一个包罗了数据结构和算法的软件框架。
在之前 C 语言的数据结构专栏中,顺序表、链表等等都是需要我们自己造轮子来实现。但在 C++ 中,有 STL 就好比站在了巨人的肩膀上,可以走的更远。当我们需要使用这些内容时,无需自己重新造轮子,从而大大提高了开发效率。
1.1 STL 版本
这里我直接贴一个 C 语言中文网的链接👉【点我】
里面详细介绍了 STL 发展历程中出现的几个版本,其中 SGI 因为被 Linux 的 GCC 所使用,可移植性高。之后的博客主要是学习 SGI STL
版本。
1.2 STL 组成
同样是 C 语言中文网的资料👉【链接】,我将它整理为了下面这个思维导图
在面试中,STL 的内容也是 HR 经常考察的。所以我们一定要认真学习这一部分的知识点!
1.3 STL 的一些吐槽
如果你去 Cplusplus 网站上看过 STL 库的接口,你就会发现 STL 库的设计有些复杂。有很多地方都考虑的过于细致,导致函数接口非常多,想要全记住这些接口是有些困难的
当然,这也是我自己太菜了的缘故。或许以后用的多了,这些就理所应当的记住了吧。
同时,因为 STL 使用了模板,所以当你多次使用 STL 时(比如 vector 容器)就容易出现代码冗余
好啦,不 bb 这些没用的了,还是直接进入正题 string 类吧!
2.String
参考 cplusplus 的标准文档:string
string 类是表示字符串的字符串类,该类的接口和常规的容器基本相同,并添加了一些专门用于操作字符串的常规操作。使用 string 需要包含 <string>
和 using namespace std;
- 这里为什么是
string
而没有.h
呢? - 其实编译器处理头文件并不会关注头文件的后缀,且 C 语言中已经有一个
string.h
了,为了避免冲突,所以使用了<string>
作为头文件
下面介绍一些常用的 string 类函数接口,标题中的英文和 cplusplus
网站中的分类对应
2.1 编码格式
在 Class instantiations
栏目下,可以看到 string 有很多不同的类,这些类的主要区别在于编码方式的不同。我们主要学习的是第一个的 string
类
什么是编码格式呢?在编程学习中,比较常用的便是 ASCii
码,除此之外,还有 utf-8
、utf-16
等等。
ASII 码表中,英文单词、数字、各类标点符号都有它们对应的值,这样才能在只支持 01
二进制的电脑上显示出对应的内容。当计算机需要显示英文单词的时候,就会去查找这一个表。所以 ASCII 码表是漂亮国设计的。
我们知道,英文中的基础只有 26 个单词,算上大小写也就 52 个。但是我们中华文化博大精深,计算机需要显示中文的时候,一个 char 类型的空间已经不够。所以我们需要整出一个我们自己的编码格式,以此让计算机支持显示中文 ——GBK
就是这样一个编码格式
GBK 使用两个字节来存储一个汉字,一些不常用的生僻字可能需要 3-4 个字节来存储。
用下面这个简单的函数来测试,我们可以发现,中文中谐音字的编码是相近的
在网络上,我们打某些词汇会被替换成 ****
,就是程序在后台实别了你的编码。同时如果想进行模糊匹配,把谐音字也屏蔽掉的话,就可以把这个词周围的编码全部屏蔽了。
同理,
utf-16
和utf-32
为了支持别的国家的语言,就会用更长的字节来存储文字。这里不进行详解。
string 类有非常多的接口,我们并不需要完全掌握所有的函数接口。只需要学会常用的接口,在遇到一些不常用的,在需要使用的时候可以去查找 cplusplus 的文档。
2.2 构造函数 (constructor)
下面是一些常用的 string 类的构造函数
构造函数 | 功能 |
---|---|
string() | 空的 string 对象(空的字符串) |
string(size_t n,char c) | string 类对象中包含 n 个字符 c |
string(const char*s) | 利用常量字符串来构造对象 |
string(const string&s) | 拷贝构造 |
除此之外,在文档中我们还可以看到更多构造函数
除此之外,我们还可以调用赋值操作符进行构造。下面是赋值重载的 3 个版本,想必都能看懂,是通过对象、常量字符串和字符进行赋值操作。
string 类中也重载了流提取和流插入操作符,方便我们直接对对象进行输入输出操作。
我们还可以选取一个范围进行构造,比如下面这个
需要注意的是,该构造函数的第三个传参有缺省值 npos
1 | string (const string& str, size_t pos, size_t len = npos); |
查文档可以看到,nops
其实是 - 1,而它的类型是无符号整型,-1 就代表无符号整形的最大值。即从 pos 位置开始,往后取最大值的长度(实际上压根没有那么长的字符串)
2.3 析构函数 (destructor)
析构函数有个好处,就是编译器自己会进行调用,我们只需要简单了解即可。
2.4 遍历 string 对象
我们可以通过下面的 3 种方式来遍历一个 string 对象
1 | void test3() |
可以看到这三个方式都成功打印出了 s1 对象的完整内容
其中范围 for 编译器在操作的时候是用 迭代器来实现的,这一点通过查看汇编可以看出来
2.5 operator [] 和 at(Element access)
上面我们用到了 operator[]
重载,需要了解的是,这个重载返回的是值的引用。也就是说,我们除了可以用这个方式来访问值的内容以外,还可以通过这种方式来改变 string 中某一个位置的值。
1 | const string s1("hello"); |
at 函数的使用方式和 []
重载类似
区别就是,当 operator[]
遇到越界情况的时候,如果相等和小于长度,都不会报错。但是当下标大于长度时,会引发未定义行为
而 at()
的处理方式是,只要长度不小于 string 的长度,就抛出异常
2.6 正向和反向迭代器 Iterators
除了在 2.2.3
中使用过的正向迭代器以外,string 还有一个反向迭代器 rbegin
1 | //反向迭代器 |
这里需要注意的是,虽然这个迭代器是反向的,但是我们使用的时候,依旧会 rit++
而不是减
注意,基本的迭代器是可读可写的。在 string 里面还实现了 const 的迭代器
1 | const_iterator begin() const; |
如果你觉得这样写太麻烦,而且容易记不住。可以让 auto
来自动进行推导
1 | const string s1("hello"); |
C++11 中,为了和基本的方式进行区分,新增了以 c 为前缀的 4 个迭代器。其使用和 const_iterator
是没有区别的。
2.7 长度和容量操作 Capacity
2.7.1 size 和 length
这其中 size 和 length 的功能完全相同。只是早期 string 设计的时候以 length
作为字符串的长度。在新版本中为了和其他接口比如 List 进行同步,又新增了一个 size 来表示字符串长度。
2.7.2 resize 和 reserve
注意区分 reserve 和 reverse
我们可以通过 reserve
对内存进行扩容操作,容量变大是因为需要内存对齐
但在实际应用中,当字符串的容量快要写满的时候,程序会自动进行扩容,大概是 1.5 倍
而 resize
的操作是修改 string 类的长度 size,并同时进行扩容
1 | string s1("hello"); |
通过调试可以发现,这里会把 size 修改为 100,并将多余内容全部初始化为 0
我们还可以给 resize
进行传参,指定初始化的内容
同时,这两个函数一般都不会对容量进行缩容
但是 resize
会修改 size 的大小,即抛弃掉 10 以后的内容,但保持容量不变
需要注意的是,在 VS2019 中(不同编译器可能不一样),reserve 如果传参小于 15,则会对容量进行缩容到 15(string 对象默认会开辟 15 个字节的 capacity)
2.8 修改内容(Modifiers)
string 可以通过很多方式来增加、删除内容
2.8.1 尾插
它们的基本使用如下,其中最方便的肯定是 +=
操作了,又清晰又简单!
2.8.2 中间插入
string 并没有提供一个头插的选项,而是提供了一个 Insert
,可以在任何位置进行插入
insert
函数的时间复杂度相对较高,因为在中间或者开头插入内容需要挪动数据。空间不够的时候还需要执行扩容操作,效率较低。
2.8.3 删除
可以通过 erase 函数删除数据
- 1:默认从 0 开始完全删除,可以选择从 pos 位置开始删除 len 长度的数据
- 2:利用迭代器删除 p 位置的内容
- 3:删除一个范围的数据,从 first 开始 last 结束
这个很容易理解,在这里就不做演示了
2.8.4 替换
这个函数使用并不频繁,其修改操作不如使用拷贝复制😂
比如其中第二个函数的作用是将 str对象
中,从 subpos
位置开始的 sublen
长度内容复制到本对象中。
2.8.5 交换
在 string 类中有一个交换函数,同时,std 标准库里面也有一个交换函数
1 | s1.swap(s2);//string类 |
- string 类里面的函数是交换类对象的指针
- 而标准库里面的 swap 函数需要进行深拷贝交换
所以 string 类里面的 swap 函数在处理对象的时候,比标准库里面的 swap 效率会高一些
2.9 字符串操作(String operations)
2.9.1 c_str
这个接口的作用是返回一个字符串的指针,其主要是为了和 C 语言的一些函数对应,比如利用 strcmp
拷贝一个 string 对象到内置字符串 char arr[]
中。
2.9.2 find
这里可以看到非常多种类的查找函数(偷懒不写示例了)
find
函数可以查找 string 中的某一个字符或者字符串,并返回起始位置的下标- 该函数默认从头开始查找,你也可以单独指定从 pos 位置开始查找
- 和迭代器一样,
rfind
则是从末尾开始找指定内容
有些时候我们需要查找的内容并不是从头开始的,所以就需要从尾部开始找。
- substr 是从指定 pos 位置开始获取长度为 len 的子串
2.10 很多操作符重载
string 里面有非常多的操作符重载,支持和字符串、字符、对象进行大小对比。虽然看的有点麻了,但实际上它们只是方便我们使用。底层实现了解一下就可以了(我这是不是废话…)
其实一部分内容都是可以通过编译器的隐式类型转化或者临时构造一个 string 类来实现的,但是设计 string 的大佬们显然觉得多即是好,哈哈。
3. 模拟实现
要想切切实实弄明白 STL 的源码结构,其中一个最好的办法就是尝试模拟实现一个和 std 库里面 string 使用方法 / 功能相同的轮子。
我的 string 模拟实现源码已经托管到 gitee 了【链接】
其实 string 和 vector 就是一个简单的顺序表,其二者的底层差距主要在 string 只能保存字符(串),当然,内部的接口有一些细微的差距,这会在 vector 的博客里面进行讲解。
大部分函数可以直接在 gitee 上面查看,我写了还算靠谱的注释,有任何问题可以在 gitee 或者本博客下留言。
3.1 利用指针模拟实现迭代器
从 string 开始,我们第一次接触了 stl 中的迭代器这一新鲜玩意。
1 | //反向迭代器 |
看起来很牛,实际上它的操作和我们自己写的指针大差不差。我们大可以直接使用指针来模拟实现出一个相同功能的迭代器!
1 | //迭代器相关 |
看吧,是不是很简单!🌃这个迭代器的使用方法和顺序表中的指针一模一样,这里就不演式啦!感兴趣的伙伴可以把源码考下来自己试试。
3.2 深拷贝
在之前类和对象的博客中,我已经简单实现过一次深拷贝(链接)
对于 string 来说,深拷贝的操作和博客里面演示的是一样的,都是通过给予新的空间,然后用 strcpy
或者 memset
拷贝内容过去。
1 | //拷贝构造 |
除了拷贝构造以外,赋值重载同样需要深拷贝操作,但是它的操作和构造函数又有些不同
- 拷贝构造时,该对象还不存在,通过构造函数构造出内容
- 赋值重载时,该对象可能已经有自己的数据,需要先释放已有的才能获取新的
这就需要我们在赋值重载的时候,先 delete
掉原本的内容,再去进行赋值
1 | //深拷贝赋值重载 |
这样我们的赋值重载便也实现完毕了
3.3 关于 insert 函数操作字符和字符串的区间问题
当我们使用 insert 来处理字符的时候,需要将插入位置之后的数据往后挪动。
而 insert 字符串的时候,则需要挪动该字符串长度的数据。
这时候就非常容易出现挪动不完整 / 越界等等问题!
关于 insert
代码可以看 gitee 代码的 190-237行
,如果你不太能理解这里的移动问题,最好的办法,就是画图!画图会让你清楚的认识到需要移动多长,指针应该指向哪里进行移动。理清思路后,写代码就会容易很多。
3.4 关于 push_back 的边界控制问题
之前我实现的代码中,尾插的时候没有加上 \0
进行边界控制
这一点需要注意,因为模拟实现的底层用的就是 C 语言的字符串。在每一次尾插之后,都需要用 \0
来标记字符串的结尾。否在我们拷贝构造和 reserve
的时候使用 strcpy
就会出现越界错误。
越界错误一般都是在析构
delete
的时候检查出来的
1 | void push_back(char ch) |
结语
关于 string类
的介绍到这里就结束啦!
如果有什么新增内容的话,我会对本篇博客进行修改
如果有什么问题,欢迎评论区提出哦!