一、前言
本文我们就要来说一说STL中的string类,这也是我们在写C++代码的时候使用最多的
1、C语言中的字符串
首先要来谈的一点就是为什么要学习string类
string
意为字符串,那回忆一下我们在C语言阶段所学习的字符串,是以'\0'
结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,如果忘记了就再看看 字符串操作函数- 但是这些库函数与字符串是分离开的,不太符合 OOP 的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问
因此,在C++中专门搞出了一个与字符串相关的类,我们知道C++是面向对象的,我们可以在类内去写各种成员函数来对外提供操作字符串的接口,这个类就是string
类
2、string类的使用场景
那这个string有什么实际的应用场景呢,这可多了去了,如果经常刷题的同学应该瞬间能反应过来
- 从下面的两道OJ题就可以看出有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数
二、初步认识string类
1、概述
- 我们来到string类的文档中可以看到它确实是一个类,而且是由一个模版类
basic_string
所实例化出来的
- 如果你有学习过 C++模版 的话就可以知道这个类即为【类模版】
- 然后由这个类模版就实例化出了很多的模版类,其中就包含我们本文所要学习的string类
那有同学问,下面的这几个【wstring】、【u16string】、【u32string】是什么呢?
- 这一块的话要涉及到编码相关的知识,如果读者对这一块没什么接触的话就当了解一下
对于编码而言我们谈到最多的就是 ASCLL码 ,它的全称叫做【美国信息交换标准代码】,总共包含了像大小写的中英字母、数字、标点符号等等共128个,用来表示老美的的一些文章字符完全足够了。
而对于ASCLL码来说,是存在一个东西叫做映射表,即一个ASCLL码值就对应一个字符
我们可以到VS中来观察一下,其实在内存中所存放的都是ASCLL码,只不过呢在显示的时候却转换为了表中所对应的字符。这个63 64 65 66
其实是十六进制的写法,转换为十进制即为97 98 99 100
,那它们所对应的字符即为a b c d
如果有同学还是不相信的话可以看看下面这个,我给str[0]
这个位置放了数值为97,但是呢其为一个string类的对象,所以里面所存放的都是字符,可以看到在打印出来后的结果就是97所对应的ASCLL码值a
但是呢对于我们的一些中文汉字却是远远不够的,例如说下面这个”比特”
,我通过【sizeof】打印出了其在内存中所占字节数,发现有5B,这是为什么呢?原因就在于汉字的存储规则不是按照ASCLL码来的,而是专属于我国的一套编码字符集叫做GBK。在【GB2312-80】中就存储了很多有关汉字的规则
一般来说一个汉字对应的2个字节,这里的”比特”
是因为最后还有一个\0
最后对这个string类的特性做一个总结:
- string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:
basic_string
模板类的别名,typedef basic_string string;
- 不能操作多字节或者变长字符的序列。
⚠ 在使用string类时,必须包含#include
头文件以及using namespace std
2、常用接口细述
对string类大体有个了解后我们就要去学着使用这个类里面的一些函数了
1)string类对象的默认成员函数
函数名称 | 功能说明 |
constructor | 构造函数 |
destructor | 析构函数 |
operator= | 赋值重载 |
==① 构造函数==
- 点进第一个成员函数
Constructor
就可以看到存在7个重载,其中我框出来的三个是重点,要着重记忆,其余的了解一下即可
- 我将上面的函数整理了一下,方便观看
string(); // 构造一个空字符串 string (const char* s); // 用C-string来构造string类对象 string (const char* s, size_t n); // 用C-string的前n个字符来构造string类对象 string (size_t n, char c); // 生成n个c字符的字符串 string (const string& str); // 利用原先的字符串做拷贝构造 // 拷贝str字符串中从pos位置开始的len个字符 string (const string& str, size_t pos, size_t len = npos);
- 然后我们到VS里演示一下
- 前面的几个都好理解,来讲一下最后的这一个,可以看到在函数形参这一块给出了一个缺省值叫做
npos
,可能有的同学不清楚这个npos
是什么东西
// 拷贝str字符串中从pos位置开始的len个字符 string (const string& str, size_t pos, size_t len = npos);
- 这个我们也可以到文档中来查看一下,发现它是【无符号整数的最大值】
- 那这个值究竟是多少呢,我们可以将其打印出来看看
- 也可以到Linux平台下来查看发现不太一样,因为在Linux下使用的是g++编译器,它们底层所定义的规范是不一样的
💬 但是呢,无论这个数值是多少,其实影响都不大,因为我要说明的是即便我们不给出这个参数的话,编译器默认就是从当前的这个【pos】位置开始一直到字符串末尾
- 继续通过文档来观察一下,
substring
指的就是子串的意思,意思即为 从pos位置的len个长度去拷贝字符串的一部分(如果str字符串太短或者len为npos则直接到达字符串的末尾)
那现在的话你应该可以明白最后的这个参数我为何没有传递却拷贝到了后面的所有字符
- 接下去呢我们来讲一下赋值重载,这个我们在讲 类的六大成员函数 有提到过这个,它只能出现在类内但是不可以出现在类外,否则会产生一个冲突
==赋值重载==
string& operator= (const string& str); // 将一个string对象赋值给到另一个 string& operator= (const char* s); // 将一个字符串赋值给到string对象 string& operator= (char c); // 将一个字符赋值给到string对象
- 同样,我们到VS中来做一个测试,可以看到这三种方式都可以构成一个string的对象,不过照这么来看后面的这两种似乎没多大用处,用的最多的还是第一种
2)string类对象的常见容量操作
接下去我们来讲一讲有关string类中有关容量的一些操作
① size、length、capacity
- 首先要来讲的是【size】和【capacity】,这其实就和我们在顺序表中所介绍的意思差不多,前者表示当前字符串已经存放了多少数据,后者表示当前这个字符串可容纳的空间数
- 我们到VS下通过调试来观察一下,可以看到现在这个
str
没有任何的数据,而VS为我们开出了大小默认为15的空间,其实这里应该是16,只不过最后的\0
也占了一个大小
- 然后去构建出一个具体的字符串来进行观察,发现
size
的值发生了一个变化。不仅如此,我还打印了一下这个字符串的length
属性,观察到其和size
是一样的
- 然后我们点进到这两个函数的定义中可以发现它们的定义竟然是一样的
- 我们在文档的时候,一定会有一种很奇怪的感觉,在C++的容器里面为什么没有
string
呢?这里要追溯到STL的一个诞生历史,其实对于string
这个类,是不属于STL的,因为它是在STL之前就已经存在了的,属于C++标准库里的内容
- 像下面这样来看的话,STL是属于标准库中的一部分,
string
也是属于标准库的一部分,但是呢二者却没有什么直接的关联。在最早期string
的长度接口是定义为【length】的,到后面STL出了之后才改为了【size】,但是这一块我们需要去考虑到一个历史追溯的问题,所以不能将这个接口给删除,因而我们才看到一模一样的两个接口实现
💬 后续我们在使用到时候直接用【size】即可,因为其他STL库中的接口使用的也是【size】
追究完这个【size】之后,我们再来谈谈这个【capacity】
- 我们使用的是下面这段代码进行测试
void TestCapacity() { string s; size_t sz = s.capacity(); cout << "making s grow:\n"; for (int i = 0; i < 100; ++i) { s.push_back('c'); if (sz != s.capacity()) { sz = s.capacity(); cout << "capacity changed: " << sz << '\n'; } } }
- 以下分别是在Windows和Linux平台下的运行结果,通过不断往这个
s
中添加字符来扩充他的容量
💬 可以看到Windows下的VS中,扩容的大小是呈现一个1.5倍大小的趋势;可是在Linux平台下呈现的却是2倍大小的趋势
- 这其实就是因为不同平台下这个STL库的内部实现是不一样的,甚至都是在VS中,不过呢使用不同版本的VS其大小也是不一样的
② maxsize
接下去呢我们再来说说这个【maxsize】
void TestMaxsize() { string s("hello"); cout << s.size() << endl; cout << s.max_size() << endl; }
- 一样,我们放在这个双平台下演示一下,发现结果也是各不相同
③ clear
接下去呢,我们来讲【clear】
void TestClear() { string s("hello"); cout << "size: " << s.size() << endl; cout << "capacity: " << s.capacity() << endl; s.clear(); cout << "size: " << s.size() << endl; cout << "capacity: " << s.capacity() << endl; }
- 这次双平台下演示的结果就一样了,并没有什么差异,对于
clear
来说,它只会清空字符串中的【size】,但对于【capacity】来说是不会有什么变化的,如果你是这个接口的实现者一定不会去清空这个【capacity】容量的大小,万一后面又需要插入字符呢?此时又需要去进行一个扩容,是非常麻烦的
④ empty
- 接下去再来看看【empty】这个接口,很简单,我们复用一下上面这段代码,看一下这个字符串在清空前后的【empty】结果即可,于是就可以看到没被清空的时候返回
0
,代表false
不空,而被清空之后返回1
,代表true
为空
⑤ reserve
接下去呢我们来讲讲这个【reserve】,可以提前为一个字符串开出指定的空间
- 这一块我们可以放在之前观察【capacity】的地方进行测试,可以发现在开出预留空间后就不会去执行下面的这一段扩容逻辑了,因为此时空间已经够了
- 可以看到我们在VS下给定预留空间为100的时候此时系统给我们的大小为【111】,很明显是要多出一点的,但是在Linux下去运行的话就不是这样了,而是给多少开多少,就给了我们【100】的空间
💬 那有同学问:这是为什么呢?
- 原因也是一样,平台不同实现不同,所以两个平台都会去做一个演示
可能上面这样还不是很好理解,我们举个例子来理解一下
- 这个月呢你想买件衣服,但是没有零花钱了,于是就是找妈妈要零花钱,总共要个100块,那你妈妈觉得既然衣服都买了的话,裤子也一起买吧,于是直接给你打了200块
- 此时这种行为就和VS下是一样的,虽然你只是要了100空间的大小,但是呢编译器却给了你111
- 那像Linux里这种行为就是你要多少钱你妈妈就给你打多少,一分不多、一分不少,要开100个容量大空间就给你100
我们也可以到文档中来看看
- 说如果参数中这个【n】比当前容量的大小来得大的话,那么会增长到n个字符的空间大小
不过呢这个文档下面还有一部分,那就是不仅会发生【增容】的情况,也会发生【减容】的情况
- 我们可以在最后的地方加上
s.reserve(10)
来试试是否可以,却发生容量没有发生变化
- 但是呢,当我到Linux平台下再来演示的时候,却发现有了不一样的变化,确实发生了【减容】的情况
- 其实在这里就对应了文档中所说的【non-binding】,即不可控的
可是,我接下来的操作,可能就会让你惊掉下巴(⊙ˍ⊙)
- 可以看到我在即将【减容】前使用了
clear
将字符串中的数据清空后,此时再去使用reserve
的时候容量就发生了变化,回到了最初的那个默认大小
- 那到Linux下来看看发现也会其作用,不过是缩容到指定的大小,而不是默认的大小
- 再
reserve(0)
一下看看果然也是这样
从以上的种种结果来看可谓真的是【变化多端】鸭🦆,多然是不好掌控
⑥ resize
void resize (size_t n); void resize (size_t n, char c); // 初始化数据为n个c字符
- 讲完【reserve】后,我们再来讲讲【resize】,它们很类似,但也有不同之处
void TestResize() { string s("abcdef"); // 开空间 s.reserve(100); cout << s.size() << endl; cout << s.capacity() << endl; cout << "---------------------" << endl; // 开空间 + 填值初始化 s.resize(200); cout << s.size() << endl; cout << s.capacity() << endl; }
- 通过对它们两个做一个对比可以发现对于【reserve】而言仅仅只是对
capacity
起作用,size
就是初始化的字符串个数; - 但是对于【resize】而言
size
和capacity
会一起发生改变,前者变为传递的参数n,后者则遵循VS下的扩容规则,比给出的参数多一些
一样,我们通过解读文档的形式来看看这个函数还有哪些内容
- 如果 n > 当前的字符串长度,则在末尾插入所需数量的字符以达到n的大小来扩展当前内容
- 如果n < 当前的字符串长度,则会去进行一个删除的操作,删除第n个字符之后的字符
第一点我们刚才已经验证过了,接下去我们再来试试第二点,看看是否真会去做一个删除的操作
- 可以看到,若是传递了 < n的参数进去的话,再去打印这个字符串就会发生一个截断的现象
上面我们所使用的都是第一个只传递n,不过下面还有一个重载形式可以【初始化数据为n个c字符】,看到最后这里也有说到如果这个c
是特殊的话,就会去以这个c
字符去填充后面多开空间的部分
- 可以看到若是我们没有去制定后面这个字符
c
的话,默认会以'\0'
进行填充的,那如果我们制定出字符呢?
- 此时我指定了后面的字符
c
,此刻后面填充的就是我们所指定的那个字符
s.resize(100, 'a');
⑦ shrink_to_fit
- 最后我们再来讲一个,它也可以做到一个减容的效果
void shrink_to_fit();
- 立马我们就来试一试它究竟能起到什么样的效果,发现在使用后可以让原本在【resize】之后不符合大小的
capacity
变得适应size
的大小, 确实也有一些作用,不过使用的场景不多,读者了解一下即可