前言:
在上期,我们简单的介绍了关于 模板和STL ,今天我就带领大家学习一下关于 【string】类。本期,我们主要讲解的是关于 【string】的基本介绍以及【string】类的常用接口说明。有了以上的基本认识之后,在下期,我们将模拟实现一个【string】类。
前言:
string 是C++里面我们最常见的类之一,管理的是字符串。那什么是最常见的呢?
- 传统的类型,如内置类型只能表示一些基础的信息,当需要表示一些复杂的信息时就不适用了;
- 假如我们要表示地址,表示身份证号码,此时再用日常的类型则无法较好的表示出来;
(一)为什么学习string类?
1、 C语言中的字符串
基于上述情况,在C语言中引入了字符串来管理
- C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数;
- 但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
(a)string类的引出🔥
💨 假如此时我们的搬家了,这时需要修改家庭住址,地址信息变长了,这就会导致原先的字符串数组存放不下这么多的信息。
💨 因此在 C语言 中不能很好的进行管理,在C++ 中就提出了【string】类来管理字符串。
2、两个面试题(暂不做讲解)
在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本 都使用string类,很少有人去使用C库中的字符串操作函数。
(二)标准库中的string类
1、 string类(了解)
首先,在正式的介绍之前,我先给大家说明一点:
- 在接下来的学习中,我们要学会去浏览权威的文档,对于那些全是英文的文档,大家不要害怕。作为一个合格的 “程序猿” ,学会看文档是必不可少的一项技能。
【string】文档介绍:string类的文档介绍
接下来,我通过文档简单的介绍一下【string】,看文档里面是怎么“解释说明的”。
此时可能就会有小伙伴有 疑问了。我介绍过说【string】是一个模板。但是根据之前的学习觉得这个跟模板没多大的关系啊似乎?
- 其实【string】确实是一个模板,只是被 typedef 出来的而已,当我们打开文档时。
我们可以发现如下字眼:
- 此时,当处于目前页面的文档往回退一页时,大家可以发现如下字眼:
- 由于一些历史的原因,string 还提供了 宽字符的存在,紧接着由于 C++11 的提出,又新增了两个,分别是【u 16string】和 【u 32string】
那么此时就有很多小伙伴有疑问了,为什么要引入这么多呢?
💨 其实,大家在这一部分要这么理解。大家在看待这个【string】的时候其实就像我们之前学习的 “顺序表” 一样,底层管理的是一个字符数组,只是支持增删查改;
因此,换句话说,我们可以这样理解:
- 【string】 :管理的是 “char”类型的数组;
- 【wstring】:管理的是 “wchar” 类型的数组;
- 【u 16string】和 【u 32string】:管理的是 “char16_t”和“char32_t” 的数组。
区别在于 :
- 【string】:一个字符表示一个字节
- 【wstring】和 【u 16string】:一个字符表示两个字节
- 【u 32string】:一个字符表示四个字节
因此,至于为什么引入这么多。其实目的只有一个就是为了达到管理不同字符数组的需求。
2、编码
(a)ascll码
理解上述问题之后,此时又引出了一个问题。
- 那就是为了达到管理不同的字符的需求,我们需要先理解 “编码” 的基本知识!!!
对于 “编码” 这个东西,我相信大家在之前肯定已经听说过了,对其都有或多或少的了解。那大家知道,我们第一个接触的编码是什么吗?
- 其实就是在平常学习中听得最多的 ASCLL编码 了。
此时我们要显示英文是不是很简单啊!因为英文最主要的就是由以下几部分组成:
- 26个字母,如果区别大小写,那就是52个;
- 在加上数字;
- 最后就是标点符号
了解以上之后,此时我问大家计算机能否直接存储以上这些信息呢?
- 铁铁的是不能的,因为在计算机内存里面一切皆是二进制的 0和1 ;
- 所以基于上述原因,需要建立一张对应关系的映射表,因此美国基于常见的字符建立了一张ASCll表
- 假设我要存字符 ‘a’ ,此时我就去查这张表,对于‘a’,在表里面映射的就是 97,因此只需存入一个97 在计算机里面即可。
接下来,我们通过代码演示一下:
解释说明👇
- 在上述的代码中,我们写了一段简单的代码;
- 此时我们调试起来,我没有使用监视窗口,我直接使用底层的内存去查看;
- 大家可以发现,对于我们给出的 “apple” ,在内存中,首字节的 61 即表示 “a”因为在内存下是16进制,转换过来就是 “a”对应的ascll码值 97;
- 相同的,后面的字符依次按照这个规则,就可以验证上述编码表的内容。
(b)万国码
但是此时就遇到一个问题,假如我们就照搬这个ascll码,中文是否可以显示呢?
- 答案很明显,中文要在计算机上显示是十分困难的。
- 对于美国这套相对来说还还是很简单的,就那么些符号,要显示文字就用符号组成;
- 但是对于我们中国文字来说,我们走的是象形文字的路线。汉字都有差不多十万个,对于美国那套就像是一个汉字由一个符号组成,但是我们每个汉字都是独立的意思;
- 还有数量多了之后,一个字节最多可以表示256个,但是我们的文字是远超于256的;
- 同时,世界上不同的国家语言文字表示还各不相同;
- 因此,基于上诉这种情况,为了能够更好的显示以及推广,就有 推出了 【Unicode】。
万国码可以表示很多国家的文字,但是同时也会出现一个分歧。
- 比如像有些国家它的文字最多上万个甚至更少,而像我们中国那就是多的数不胜数;
- 在这个地方假如有一个字节又不好表示,两个字节又太多了;
- 因此,基于这种情况万国码又开始划分为三种:UTF-8 、UTF-16、 UTF-32
UTF-8
- UTF-8以字节为单位对统一码进行编码。同时兼容 ascll码;
- 特点是对不同范围的字符使用不同长度的编码
- 最重要的一点是支持变长的,意识就是同时支持 1字节,也可以使用2字节,最多支持4字节
UTF-16
- UTF-16编码以16位无符号整数为单位。
UTF-32
- UTF-32编码以32位无符号整数为单位。
(c)GBK字库
而对于我们中国来说,由于历史的原因,导致有些文字太复杂或者其他原因,在老外的那一套框架中就没有我们想要的那个字。
- 因此,就衍生出了我们中国自己的这样一个规范,名为——GBK字库
假如此时我们写了这样的一段代码
int main() { char str[] = "apple"; char str1[] = "中国"; cout << sizeof(str1) << endl; return 0; }
大家知道对于 【str1】的大小是多少吗?
我们在通过调试去观察
解释:
对于 str1 显示的为什么是负的呢?
- 很简单,因为它要兼容ascll,因此它在这里运行的时候不能用正的值,就是第一个比特位是 0的那个要兼容ascll,它是 char 那个系列的;
- 此时对于 char的第一个字节如果是 0就去ascll中查,如果不是 0,就需要用两个组合起来去查后面这个编码表。
我们在写出这样的几行代码,再带大家看看最终的结果是什么:
int main() { char str[] = "apple"; char str1[] = "中国"; cout << sizeof(str1) << endl; str1[3]--; cout << str1 << endl; str1[3]--; cout << str1 << endl; str1[3]--; cout << str1 << endl; str1[3]++; cout << str1 << endl; str1[3]++; cout << str1 << endl; str1[3]++; cout << str1 << endl; return 0; }
- 输出结果如下:
- 从上可见,汉字在经过编码表的时候不是随便编的,他把同音字编到一起去了
对于这种情况在生活中我们可以举个简单的例子来说明,大家知道“净网行动”吧!!
- 当我们在峡谷中遇到那些打得又菜,专门来演你的人之后,此时如果你脾气好点就是打字问候对方。但是当我们输入我们想打的字时显示的却是 ‘###’这样的情况;
- 所以当我们有编码表时,由于同音词很多,我们可以打同音字。游戏厂家为了营造和谐的上网氛围就会利用这种手段把这些字全部屏蔽掉,对于做的好厂家,此时当我们在输入一些字时,连同音字都打不出来。
小结:
- 因此,计算机在C++ 里面要与时俱进的去这样发展,为了应对不同的编码,C++就引入了模板:不管你是【char】的数组,还是【wchar】类型的数组都可以支持。所以【string】类才搞得这么的复杂。
(三)string类对象的访问及遍历操作
1、遍历三剑客 🔥
首先,我们要谈的就是关于进行string遍历的三种方法:
(a)迭代器 begin()+end()
begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
- 代码展示:
int main() { //遍历和读写容器的数据 string str("hello world"); string::iterator it = str.begin(); while (it != str.end()) { cout << *it << " "; ++it; } cout << endl; return 0; }
- 但是,此时又有一个问题,那就是你这传的是普通对象,那对于 const 对象这样做是否可以呢?
- 当我们如下这样去赋值时,是否还可以去遍历字符串呢?
void Func(const string& s) { string::iterator it = s.begin(); while (it != s.end()) { cout << *it << " "; ++it; } cout << endl; } int main() { string str("hello world"); // 2.正向迭代器 string::iterator it = str.begin(); while (it != str.end()) { cout << *it << " "; ++it; } cout << endl; Func(str); return 0; }
- 当我们去编译代码时,就会自动出现报错的情况
- 那什么原因呢?我们可以去结合文档查看 begin接口
- 所以,此时我们需要使用【const】版本的迭代器:
void Func(const string& s) { //遍历和读容器的数据,不能写 string::const_iterator it = s.begin(); while (it != s.end()) { //*it += 1; //此时const迭代器就不允许进行修改操作 cout << *it << " "; ++it; } cout << endl; } int main() { string str("hello world"); // 2.正向迭代器 string::iterator it = str.begin(); while (it != str.end()) { cout << *it << " "; ++it; } cout << endl; Func(str); return 0; }
大家不难发现上述的这种方式是从前往后遍历的。
那么当我们想从后往前遍历,string 是否支持呢?
- 因此为了满足以上这种需求,在 【string】类中还引入了关键字—— rbegin 和 rend
(b)迭代器 rbegin()+rend()
- 代码展示:
string::reverse_iterator rit_1 = str.rbegin(); while (rit_1 != str.rend()) { cout << *rit_1 << " "; ++rit_1; } cout << endl;
- 此处也有 const类型,与上述的同理!!!
(c)for+[]
返回pos位置的字符,const string类对象调用
- 代码展示:
int main() { string str("hello world"); for (size_t i = 0; i < str.size(); ++i) cout << str[i] << " "; return 0; }
(d)范围for
C++11支持更简洁的范围for的新遍历方式
- 代码展示:
int main() { string str("hello world"); for (auto e : str) cout << e << " "; return 0; }
- 整体代码如下:
int main() { string str("hello world"); // 3种遍历方式: // 需要注意的以下三种方式除了遍历string对象,还可以遍历是修改string中的字符, // 另外以下三种方式对于string而言,第一种使用最多 // 1. for+operator[] for (size_t i = 0; i < str.size(); ++i) cout << str[i] << " "; cout << endl; // 2.正向迭代器 string::iterator it = str.begin(); while (it != str.end()) { cout << *it << " "; ++it; } cout << endl; // 3.反向迭代器 // string::reverse_iterator rit = s.rbegin(); string::reverse_iterator rit_1 = str.rbegin(); while (rit_1 != str.rend()) { cout << *rit_1 << " "; ++rit_1; } cout << endl; // C++11之后,直接使用auto定义迭代器,让编译器推到迭代器的类型 auto rit_2 = str.rbegin(); while (rit_2 != str.rend()) { cout << *rit_2 << " "; ++rit_2; } cout << endl; // 4.范围for for (auto e : str) cout << e << " "; cout << endl; return 0; }
2、单个字符
- 不仅如此,除了上述的可以遍历整个字符串之外,string还可以遍历一个字符。具体如下:
int main() { string str("hello world"); cout << str[8] << endl; }
- 我们可以通过输入不同的数字下标来达到遍历每个字符的目的。
- 同时我们还可以修改字符串中的某个字符:
int main() { string str1("hello world"); //对str1,可以修改字符串中的某个字符 cout << str1[8] << endl; str1[8] = 'E'; cout << str1 << endl; return 0; }
- 本来str[8]的位置的字符是 r,经过我们的手动修改,就把原本的 字符‘r’ ,改为了‘E’,输出结果如下:
- 但是以上对字符串进行修改的操作,在const类型下则是不可以的,编译器会提示报错:
int main() { const string str2("Hello world"); //对于str2,则不能修改字符串中的某个字符 cout << str2[8] << endl; str2[8] = 'E'; cout << str2 << endl; //编译失败,因为const类型对象不能修改 return 0; }
- 当我们运行代码,最后编译器会提示报错:
- 整体代码如下:
int main() { string str1("hello world"); const string str2("Hello world"); //对str1,可以修改字符串中的某个字符 cout << str1[8] << endl; str1[8] = 'E'; cout << str1 << endl; //对于str2,则不能修改字符串中的某个字符 cout << str2[8] << endl; str2[8] = 'E'; cout << str2 << endl; //编译失败,因为const类型对象不能修改 return 0; }
(四)string类对象的常见构造
- 整体代码如下:
int main() { string str1("hello world"); string str2="hello world"; //构造和str1一样的效果 string();//构造空的string类对象,即空字符串 //复制构造函数,构造 str1 的副本。 string s1(str1); cout << s1 << endl;//hello world //子字符串构造函数 //复制 str1 中从字符位置 8, 开始并跨越 3 字符的部分 string s2(str1, 8, 3); cout << s2 << endl; //rld //复制 s3 指向的以 null 结尾的字符序列 //表示复制 s3指向的前六个字符 string s3("have a nice day", 6); cout << s3 << endl; //have a //范围构造函数 //以相同的顺序复制区域【str1.begin(), str1.begin() + 7】 中的字符序列。 string s4(str1.begin(), str1.begin() + 7); cout << s4 << endl; //hello w //填充构造函数 //用字符 x 的 10 个连续副本填充字符串,string类对象中包含10个字符x string s5(10, 'x'); cout << s5 << endl; //xxxxxxxxxx string s6(10, 42); cout << s6 << endl; //********** return 0; }
(五)string类对象的容量操作
接下来,我们将要学习的便是关于 【string】类的容量操作了。主要学习的包括以下几个基本操作的内容:
1、size() 和 length()
大家看到上述的表之后,可能会觉得奇怪。我第一个写出来的是 size() ,为什么还要有一个 length() 呢?
- 别着急,我们通过代码具体感受一下二者:
int main() { string str1("hello world"); cout << str1.size() << endl; cout << str1.length() << endl; return 0; }
- 上述的代码大家觉得最后的结果是一样的吗?我在这就不卖关子了,直接编译代码,最终结果如下图所示:
- 从上我们不难看出,二者的功能都是相同的;
那这两者到底有什么关系呢?我给大家浅浅的解释一下:
对于这个string 呢,其实还涉及到一个发展历史。具体是什么呢?
- 其实string 相对STL的出来还要早一些,严格来说是不属于STL的,它不是在STL下产生的,而是在C++标准库下产生的;
- 最开始的出现的时候呢,它的名字就叫做【length 】,最开始设计的时候对于字符串使用 【length】是不是很符合这样的需求;
- 之后随着STL的发展,因为在标准库之中已经有了这样的一个雏形了,所以 STL没有加它,但是从功法用途上就是一个数据结构;
- 为了跟其他的数据结构保持一致,对于顺序表,链表这样的用【length】还说得过去,但是对于 “树”这样的数据结构却是显得不合适的;
- 因此,基于上述这样的原因便引出了 size()。size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一 致,一般情况下基本都是用size()。
2、capacity()
很明显,主要的功能就是 :返回已分配存储的大小
- 代码演示:
int main() { string str1("hello world"); //capacity cout << str1.capacity() << endl; //16 return 0; }
注意:
- 其实呢,对于上述的字符串分配的空间的大小。严格意义上来说是 【16】。
- vs下面的这个空间是不包含 \0 的,它不认为 \0 是有效字符,它认为 \0 是标识字符,string的结果使用 \0进行表示的。
- 接下来,我通过监视窗口带大家瞧一瞧
3、empty()
顾名思义就是判断字符串是否为空的一个接口函数。如果字符串长度为 0,则为 true,否则为 false。
4、clear()
主要功能就是 :清除字符串
- 代码展示:
int main() { string str1("hello world"); cout << str1.size() << endl; //11 cout << str1 << endl; //hello world str1.clear(); cout << str1.size() << endl; //0 cout << str1 << endl; //被清空了 return 0; }
- 结果展示:
- clear()只是将string中有效字符清空,不改变底层空间大小。
5、resize()
主要功能:调整字符串大小。相当于【开空间+初始化】
常见三种用法
- 1、如果n小于当前的容器大小,那么则保留容器的前n个元素,去除(erasing)超过的部分。
- 2、如果n大于当前的容器大小,则通过在容器结尾插入(inserting)适合数量的元素使得整个容器大小达到n。且如果给出val,插入的新元素全为val,否则,执行默认构造函数。
- 3、如果n大于当前容器的容量(capacity)时,则会自动重新分配一个存储空间。
- 代码展示:
int main() { string str1("hello world"); // 将str中有效字符个数增加到12个,多出位置用'X'进行填充 // “xxxxxxxxx” str1.resize(12, 'X'); cout << str1.size() << endl; //12 cout << str1.capacity() << endl; //15 cout << str1 << endl; //hello worldX //将str中有效字符个数增加到15个,多出位置用缺省值'\0'进行填充 // "xxxxxxxxxx\0\0\0\0\0" // 注意此时s中有效字符个数已经增加到15个 str1.resize(15); cout << str1.size() << endl; //15 cout << str1.capacity() << endl; //15 cout << str1 << endl; //hello world cout << endl; // 将str中有效字符个数增加到20个,多出位置用缺省值'X'进行填充 //注意此时容量的变化 str1.resize(20 ,'X'); cout << str1.size() << endl; //20 cout << str1.capacity() << endl; //31 cout << str1 << endl; //hello worldXXXXXXXXX cout << endl; // 将str中有效字符个数缩小到5个 str1.resize(5); cout << str1.size() << endl; //5 cout << str1.capacity() << endl; //15 cout << str1 << endl; //hello cout << endl; return 0; }
注意:
- resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个;
- 不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的 元素空间。
- resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
- 如果发生了重新分配,则使用容器的分配器分配存储空间,这可能会在失败时抛出异常。
6、reserve()
- 代码展示:
int main() { string str1("hello world"); //测试reserve是否会改变string中有效元素个数 str1.reserve(100); cout << str1.size() << endl; cout << str1.capacity() << endl; cout << endl; // 测试reserve参数小于string的底层空间大小时,是否会将空间缩小 str1.resize(100); cout << str1.size() << endl; cout << str1.capacity() << endl; return 0; }
- 因此,有了这样的特性,我们就可以使用 reserve 来提高插入的效率了
int main() { // 利用reserve提高插入数据的效率,避免增容带来的开销 string str; size_t sz = str.capacity(); cout << "making str grow:\n"; cout << "capacity changed:" << sz << endl;; for (int i = 0; i < 100; ++i) { str.push_back('X'); if (sz != str.capacity()) { sz = str.capacity(); cout << "capacity changed: " << sz << '\n'; } } }
注意:
- reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于 string的底层空间总大小时,reserver不会改变容量大小。
- 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
- 两者的比较如下:
int main() { string str1("hello world"); str1.reserve(100); cout << str1.size() << endl; cout << str1.capacity() << endl; cout << endl; string str2("hello world"); str2.resize(100); cout << str2.size() << endl; cout << str2.capacity() << endl; return 0; }
- 运行结果如下:
现象解释:
- 从上述我们可以发现,resize 不仅把字符串的 size给改了,而且还把capacity也给改了;
- 而reserve只把只把 capacity给改变,而没有改变 size 的大小。
(六)string类对象的修改操作
对于修改操作,我们主要学习的有以下几个接口功能,其余的在这暂不详解,最多带过,如果以后遇到,我们再说。
1、push_back()
- 当我们想在字符串的末尾追加一个字符时,我们就可以用到 push_back 这个函数接口。
- 代码演示
int main() { string str1("hello"); cout << str1 << endl; //5 cout << str1.size() << endl;//hello str1.push_back('X'); // 在str后插入X cout << str1 << endl; //helloX cout << str1.size() << endl;//6 return 0; }
2、append()
主要功能:追加到字符串,通过在当前值的末尾附加其他字符来扩展basic_string:
- 那此时我想追加一组字符串,上述的push_back 则不适用了,此时我们需要使用 append() 这个接口函数。
str1.append(" world"); // 在str后插入单词world cout << str1 << endl; cout << str1.size() << endl;
但是在显示中,我们其实很不喜欢这个用法,最常用的就是下面这个接口。
3、operator+=
主要功能:追加到字符串
- 代码演示
str1 += ' '; // 在str后插入空格 str1 += "wo"; // 在str1后追加一个字符'wo' str1 += "rld"; // 在str1后追加一个字符串"rld" cout << str1 << endl; cout << str1.size() << endl;
关于插入功能的函数接口大概就是以上几个,我们常用的就是最后 一种
- 整体代码展示
int main() { string str1("hello"); /cout << str1 << endl; cout << str1.size() << endl; //push_back str1.push_back('X'); // 在str后插入X cout << str1 << endl; cout << str1.size() << endl; //append str1.append(" world"); // 在str后插入单词world cout << str1 << endl; cout << str1.size() << endl; //operator+= str1 += ' '; // 在str后插入空格 str1 += "wo"; // 在str1后追加一个字符'wo' str1 += "rld"; // 在str1后追加一个字符串"rld" cout << str1 << endl; cout << str1.c_str() << endl; // 以C语言的方式打印字符串 cout << str1.size() << endl; return 0; }
4、find() 和 npos()
主要功能:查找字符串中的第一个匹配项,在basic_string中搜索由其参数指定的序列的第一个匹配项。
分析:
- 函数原理就是从pos位置开始搜索整个字符串,如果没有输入起始位置pos,则默认为0。
- 找到能成功匹配的子字符串str,如果可以找到合法存在的子字符串位置,此时需要需要注意一点的是,返回这个位置的索引坐标相对于整个字符串的起始位置而言,而不是相对于起始搜索的位置,否则返回npos.
至于什么是 【npos】,在这里我也简单的提一下:
- 首先,我们先看看文档是怎么介绍的,具体如下:
小结:
- npos: 这是一个特殊值,等于size_type可以表达的最大值,通常为无符号整型的最大值。
- 确切的含义取决于上下文,通常用来标识字符串结束或者是函数作用错误指示符。
- 在本函数当中就用来代表函数作用错误,find函数在找不到指定值得情况下会返回string::npos。
🔥用法:
- 1、例如,当我们想查找一个字符时,我们可以使用到 find(),具体如下:
int main() { string str("have a nice day"); int s1 = str.find("e"); //pos未输入则默认为0 int s2 = str.find("e", 4); cout << s1 << ' ' << s2 << endl; //3 10 if(s.find("i", 10) == s.npos) cout << s.npos << endl; return 0; } // npos是string里面的一个静态成员变量 // static const size_t npos = -1;
- 2、例如,我们想在str1 字符串中查找查找和 str2 匹配的字符串,具体如下:
int main() { string str1("have a nice day"); string str2("ce"); str1.find(str2); if(str1.find(str2) != string::npos) cout << str1.find(str2) << endl; return 0; }
5、rfind()
主要功能:是从字符串右侧开始匹配str,并返回在字符串中的下标位置
- 1、例如,当我们想查找一个字符时,我们可以使用到 rfind(),具体如下:
int main() { string str1("have a nice day"); string str2("ce"); cout << str1.find('d') << endl; cout << str1.rfind('d') << endl; return 0; }
- 2、例如,我们想在str1 字符串中查找查找和 str2 匹配的字符串,具体如下:
int main() { string str1("have a nice day"); string str2("ce"); str1.rfind(str2); if (str1.rfind(str2) != string::npos) cout << str1.rfind(str2) << endl; return 0; }
小结:
- 最后的运行结果跟 find 运行的时候结果是一样的。
6、substr()
主要功能:生成子字符串,返回一个新构造的对象,其值初始化为此对象的子字符串的副本。
- 1、例如,当我们想查找一个文件的后缀时,我们可以使用到 substr(),具体如下:
int main() { // 获取file的后缀 string file("string.cpp"); size_t pos = file.rfind('.'); string suffix(file.substr(pos, file.size() - pos)); cout << suffix << endl; return 0; }
解释说明:
- 假设此时我们有一个文件,叫【string.cpp】,当我们想查找文件的后缀时我们使用find查找到文件名的 后缀的起始,即【.】所在的位置后面的即为文件的后缀名;
- 在此处,我们通过查找到【.】所在位置,用整个字符串的长度减去pos位置之前的,得到的即为文件后缀名;
- 又因为文件后缀也是一个字符串,因此我们还用到了【substr】接口。
- 2、例如,当我们想取出一个网络的域名时,我们也可以使用到 substr(),具体如下:
int main() { // 取出DNS中的域名 string DNS("https://legacy.cplusplus.com/reference/string/basic_string/substr/"); cout << DNS << endl; size_t start = DNS.find("://"); if (start == string::npos) { cout << "invalid DNS" << endl; return -1; } start += 3; size_t finish = DNS.find('/', start); string address = DNS.substr(start, finish - start); cout << address << endl; return 0; }
(七)string类非成员函数
函数 | 功能说明 |
operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
operator>> | 输入运算符重载 |
operator<< | 输出运算符重载 |
getline | 获取一行字符串 |
relational operators | 大小比较 |
- 上面的几个接口大家了解一下,在OJ题目中会有一些体现他们的使用。string类中还有一些其他的操作,这里不一一列举,大家在需要用到时不明白了查文档即可。
(八)vs和g++下string结构的说明
注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。
💨 vs下string的结构
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字 符串的存储空间:
- 当字符串长度小于16时,使用内部固定的字符数组来存放
- 当字符串长度大于等于16时,从堆上开辟空间
union _Bxty { // storage for small buffer or pointer to larger one value_type _Buf[_BUF_SIZE]; pointer _Ptr; char _Alias[_BUF_SIZE]; // to permit aliasing } _Bx;
- 这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内 部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
- 其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
- 最后:还有一个指针做一些其他事情。 故总共占16+4+4+4=28个字节。
💨 g++下string的结构
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指 针将来指向一块堆空间,内部包含了如下字段:
- 空间总大小
- 字符串有效长度
- 引用计数
struct _Rep_base { size_type _M_length; size_type _M_capacity; _Atomic_word _M_refcount; };
- 指向堆空间的指针,用来存储字符串
(九)总结
到此,关于本文的内容便全部讲解完毕了。接下来,我们回顾一下本文都学到了什么:
- 首先,我们对为什么要学习 string类进行了解释说明。string是表示字符串的字符串类。不管是在以后的工作上还是日常练习都经常使用到 string;
- 其次,通过文档我们对标准库中的 string类进行了简单的,string在底层实际是:basic_string模板类的别名【typedef basic_string string】,紧接着还介绍了一系列的补充知识;
- 在接下来就是对 string类的常用接口说明以及讲解。该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作;
- 最后,说明一点:string 不能操作多字节或者变长字符的序列(🔥)
以上便是全文的基本内容了,非常感谢各位小伙伴的阅读!!