⚡【C++要笑着学】(16) String类:编码的由来 | basic_string 模板类 | string类的常用接口讲解 | 学会查文档

🔥 订阅量破千的火热 C++ 教程
👉 火速订阅
《C++要笑着学》 

 🔥 CSDN 累计订阅量破千的火爆 C/C++ 教程的 2023 重制版,C 语言入门到实践的精品级趣味教程。
了解更多: 👉 "不太正经" 的专栏介绍 试读第一章
订阅链接: 🔗《C语言趣味教程》 ← 猛戳订阅!

   本篇博客全站热榜排名:3  🔥

  • 💭 写在前面:我是柠檬叶子C,好久不见!前段时间比较忙,更新频率有所减缓。好在现在快忙完了,我又有时间更文咯,还希望大伙能多多支持!我将会呈现出更多高质量的博客给大家!本章将介绍 string 类,在 string 讲解之前会补充一些知识点,比如 basic_string 和字符编码等。string 类的东西太多了,全部介绍一遍似乎不太现实,所以我们将介绍 string 常用的接口函数。如果以后需要用到一些接口函数,要学会查文档,现查现用的技能。

Ⅰ. string 的前置知识

0x00 引入:学习 string 类的原因

 C 语言中的字符串,是以 \0 为结尾的一些字符的集合。 

为了方便操作,C标准库 中提供了一些 str 系列的库函数。

但是这些库函数与字符串是分离开的,不太符合面向对象的思想。

 而且底层空间需要用户自己管理,一不小心还会造成越界访问,让人很不爽。  

在工作中为了方便大多会使用 string 类,很少有人去使用 C 标准库中的字符串操作函数。

0x01 认识 string 类

📚 概念: 简单来说,string 就是一个管理字符串的类。

(下面的知识点会比较干,稍作了解 一下即可,感兴趣可以查看 sting 类的文档)

🔍 资料: string - C++ Reference(string 类的文档介绍)

① 字符串是表示字符序列的类。

② 标准的字符串提供了对此类对象的支持,其接口类似于标准字符容器的接口,

     但添加了专门用于操作单字节字符串的设计特性。

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

   (关于模板的更多信息,可以参阅 basic_string

④ string 类是 basic_sting 模板类的一个实例,它使用 char 来实例化 basic_string 模板类, 

     并用 char_traitsallocator 作为 basic_string 的默认参数。

    (关于更多的模板信息请参考 basic_sting

⑤ 注意,这个类独立于所使用的编码来处理字节。

     如果用来处理多字节或变长字符(如UTF-8)的系列,这个类的所有成员(如长度或大小)

     以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

🔺 总结:

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

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

③ string在底层上实际是:basic_string 模板类的别名:

typedef basic_string<char, char_traits, allocator>string;

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

📁 头文件: <string>

#include <string>       // 使用string类时,需引入头文件 <string>
using namespace std;    // 展开std

0x02 basic_string 模板类

在正式开始讲解 string 之前,我们还要介绍一下我们刚才提到的 basic_string 模板类。

从文档中可以看出,string 的原生类并不是直接定义了一个 string 类,而是定义出了一个类模板。

而 string 是用 typedef 出来的,它其实是 basic_string<char> 这个玩意。

💬 代码演示:我们先用 C 格式字符串构造一个 string 类对象:

#include <iostream>
using namespace std;

int main(void)
{
    string s1("string");
    basic_string<char> s2("basic_string");

    cout << s1 << endl;
    cout << s2 << endl;

    return 0;
}

🚩 运行结果:string  basic_string

既然我们知道了它的真身是  basic_string<char>  ,我们来猜想一下它在库里面是如何定义的。

💭 猜想:这个类模板可能是这么定义的

template<class T>
class basic_string {
private:
	T* _str;
	// ...
};

❓  思考:这时候我们思考一个问题,这里为什么需要模板?

有人会想,管理字符串不就是一个 char 嘛?搞一个模板 T 出来干什么?

"你以为字符串就一个 char?你以为你以为就是你以为了啊?"

这和编码有关系,这里稍微补充一下关于编码的知识。

0x03 香蕉君为你讲解:编码表的由来(听故事)

 我们知道,计算机是漂亮国的人发明的,

所以早期在计算机上自然是只需要显示英文的。

英文的显示非常简单,ABCDEFG...  英文字母一组合就是英文单词了,

大写字母 + 小写字母,再加上一些标点符号,

顶多也就一百多个字符,所以出现了一套 ASCII 码。

比如说你写了一个 'a' ,你想在计算机中存储,因为计算机只有二进制,0101010101...

虽然 0 和 1 只能表示两种状态,但是多个 0 和 1 一组合就可以表示出很多状态了。

 为了能够记录这些  自♂由的  字符,于是就建立了一个映射。

值映射符号,建立映射关系  value\rightarrow symbols ,于是就产生了编码表:

 ( ASCII 码表 )

"ASCII ((American Standard Code for Information Interchange): 美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。"

比如说我们要表示 hello,我们在数组中存储这个单词:

char str1[] = "hello";

实际在内存当中,我存的不是  {\color{Red} h,e,l,l,o}  ,而是它们每个字母(字符)在表中对应的ASCII码值。

 👈 可以通过监视观察一下

存储之后如果我们要把它打印出来,就是通过对照着 ASCII 来 "读" 这个数组的。

这,就是编码表!我们开头说了,计算机是漂亮国的人发明的,早期在计算机上只需要显示英文,

但是后来计算机开始流行,其他国家也要用,但是你这个是英文的,

虽然英文是世界通用语言,但也不是所有人都懂英文啊。

为了能让电脑更好地普及,这时候就有人搞出了 Unicode。

📚 概念:Unicode - 表示全世界文字的编码表

"统一码(Unicode),也叫万国码、单一码,是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。"

(如果你不知道并且感兴趣的话可以问问度娘)

中国文化博大精深,岂是ASCII码的256个字符能表示得了的:

"秦代的《仓颉》《博学》《爰历》三篇共3300字,汉代扬雄的《训纂篇》有5340字,许慎的《说文解字》就有9353字,晋代作品《字林》有12824字,后魏杨承庆的《字统》有13734字,唐代孙强增字本《玉篇》有22561字,到了宋代,司马光修《类篇》,里面有31319字,清代的《康熙字典》则收录了47000多字,1915年欧阳博存的《中华大字典》有48000多字,1959年,日本诸桥辙次的《大汉和词典》收字49964个,1971年张其昀主编的《中文大辞典》有49888字。"

这么多汉字都要表示,如果还是拿值和汉字一一对应,那还得了?

utf-8,常见的汉字都用2个字节去编,

一些生僻的可以用3个或者4个……(我们常说的 utf-8   utf-16   utf-32 )

你可以理解为,中文有了一套自己的规则,去找到值对应的汉字。

char str2[] = "吃饭";

存的时候存的是 utf-8 对应的值,你要用的时候它就会拿这个表去查。

要对应的上,如果对应不上就会出现我们熟知的乱码。

所以建议在 Linux 或服务器下统一把编码设置为 utf-8,不然出现乱码会很恶心。

这里显示的是 GB2312,这又是什么呢?

我们已经知道,Unicode 是世界通用编码,而我们中国也有一套自己的编码方式。

📚 概念:GBK —— 中文自己量身定做的编码表。

"GBK全称《汉字内码扩展规范》(GBK即“国标”、“扩展”汉语拼音的第一个字母,英文名称:Chinese Internal Code Specification)"

如果你打开一个文本,Windows 下一般默认的编码是GBK,Linux 下默认的就是 utf-8 。

GBK包括所有的汉字,包括简体和繁体。而这里显示的 GB2312 则只包括简体汉字。

其实,读音相同的字是编到一起的,不信我们来看一下:

int main(void)
{
	char str2[] = "吃饭";
	str2[1] += 1;
	str2[3] += 1;

	str2[1] += 1;
	str2[3] += 1;

	str2[1] += 1;
	str2[3] += 1;

	return 0;
}

知道这个特性之后有什么用呢,我瞎举个例子。

大家打游戏的时候难免爆粗口,队友太菜,嘴臭骂脏话,

你有没有把脏话发出去的时候,有些脏字变成了星号?  

想这些常见的不文明词汇,系统会有一个脏话词库,把我们的国骂都放进去。

你发了一段话,聊天系统就会检查是否和脏话词库匹配,如果匹配就会把它隐藏成 *

 (守望先锋游戏日常)

英雄联盟:草***,我上路**团战****个臭*****非要**

守望先锋:滚**亲的**东西我**消灭金,你在*叫什么?

虽然打 "我操" ,打不出来,但是人家打 卧槽、硪糙、我焯,这样的词,就能发出来了,

"wocao" 同音的字是在一个范围里的,我们利用这个特性,把跟这个范围组合的都给你屏蔽掉,

这样的话你打同音字、谐音字骂人,也能做到给你屏蔽。

 编码的知识就讲到这里,有一丢丢扯的了,

下面我们继续回到 basic_string 模板类 的讲解。

💡 回到刚才的问题 —— string 需要模板的原因:

刚才介绍了编码,现在大家应该能理解为什么 string 需要模板了。

string 不仅有 char,还有 wchar_t (2个字节),这就是 string 需要 basic_sting 的原因。

template<class T>
class basic_string {
private:
    T* _str;
    // ...
};

 所以这个 T  不一定是 char,还有可能是 wchar_t,这和编码有关系。

基于编码的原因,有些字符串就会用两个字符去表示一个字,所以就有了 wchar_t —— 宽字节

0x04 其它字符编码的 string(比如 wchar_t)

📚 概念:宽字节是一种扩展的存储方式,unicode 编码的字符一般以 wchar_t 类型存储。

💬 代码演示:我们 sizeof wchar_t 看看

#include <iostream>
using namespace std;

int main(void)
{
	cout << sizeof(char) << endl;
	cout << sizeof(wchar_t) << endl;

	return 0;
}

wchar_t 是两个字节,是为了更好地表示字符。

如果涉及 Windows 编程,Windows 下的很多接口都是用的 Unicode,

这时它的字符串不是 char*,而是 wchar_t* ,这时候就涉及到转码,

 如果想要存储 wchar_t* ,就最好用 wstring —— 专门处理宽字符的。

📚 概念:wstring 就是每个字符都是一个 wchar_t 的:

 不仅仅有 string 和 wstring!

还有 u16string(存16个比特位)、还有u32string(存32个比特位)

 🔍 这里就不逐个讲解了,感兴趣可以查文档:

u16string - C++ Reference

u32string - C++ Reference

🔺 结论:

本章主要学习 string,现阶段基本用的都是 string (里面存 char

如果碰到有些地方是 wchar_t 就要使用与之对应的 wstring 了,

其他也一样,比如有些地方字符串编码是 utf-32,这时候你就可能要用 u32string 去存储了。

因为有的库或 API只支持 UTF-16 编码的字符,

 而且有的 API 使用 UTF-16 编码的字符时执行速度会快一些。

(你如果坚持使用UTF-8,它内部需要将UTF-8转换成UTF-16,速度可能会慢)

Ⅱ. string类的常用接口

(constructor) 函数名称功能说明
string()                           (重点)构造空的string类对象,即空字符
string(const char* s)      (重点)用C-string来构造string类对象
string(size_t n, char c)    string类对象中包含n个字符c
string(const string& s)   (重点)拷贝构造函数

0x00 引入:正式开始学习 string

铺垫结束!前置知识我们已经讲完了,现在我们正式开始学习 string。

我们知道了,字符串里的每一个字符都是 char 了,那平时我们怎么用它呢?

注意,本篇博客我们只只介绍常用的接口!

0x01 string类对象的常见构造

我们还是以查看文档的方式去学习。

🔍 查看文档:string::string - C++ Reference

📚 重点掌握这几个:

string()                      // 构造空的string类对象,即非空字符串。
string(cosnt char* s)         // 用C-string来构造string类对象
string(size_t n, char c)      // string类对象中包含n个字符c
string(const string&s)        // 拷贝构造函数

💬 代码演示:

#include <iostream>
#include <string>     // 要包含头文件string
using namespace std;  // 为了方便练习,就随便展开命名空间了。

void StringTest() {
	/* 无参 */
	string s1;                      // 构造空的string类对象s1

	/* 带参 */
	string s2("HelloString!");    // 用C格式字符串构造string类对象s2

	/* 拷贝构造 */
	string s3(s2);                  // 拷贝构造s3
}

🔨 学会看文档:

#include <iostream>
#include <string> 
using namespace std; 

int main(void)
{
	string s1("abcdef123456");
	string s2(s1, 2, 6);       // 从第2个位置开始,copy 6个字符

	cout << s2 << endl;

	return 0;
}

再仔细观察,我们发现长度 len 缺省值给的是 npos

string (
    const string& str, 
    size_t pos,         // 起始位置
    size_t len = npos   // 长度(缺省值给的npos)
    );

也就是说,如果你不指定初始化多少个字符,他会默认按 npos 个来。

 这个 npos 你可以理解为是 string 里的一个静态变量,它是 -1。

当你没有指定 len 时,因为 npos 是 -1,而又是 size_t 无符号整型,他长度将会是整形的最大值。

也就是说,如果你不传,它就相当于取整型最大值 2147483647,

 这很大,非常的大,所以也就相当于取所有字符串了。

💬 代码演示:如果不指定 len

int main(void)
{
	string s1("abcdef123456");
	string s2(s1, 2);       // 从第2个位置开始,初始化npos个长度的字符
	cout << s2 << endl;

	return 0;
}

Ⅲ. sting 类对象的容量操作

函数名称功能说明
size     (重点)返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty  (重点)检测字符串是否为空串,是返回true,否则返回 flase
clear    (重点)

清空有效字符

reserve(重点)为字符串预留空间
resize   (重点)将有效字符的个数改成n个,多出的空间用字符c填充

我们先讲解其中的一部分……

0x00 让我康康字符串有多长的 size() 和 length()

 💬 先看代码:

int main(void)
{
	string s1;
	cin >> s1;
	cout << "你输入了:" << s1 << endl;

	return 0;
}

int main(void)
{
	string s1;
	cin >> s1;
	cout << "你输入了:" << s1 << endl;

	cout << s1.size() << endl;
	cout << s1.length() << endl;

	return 0;
}

 📌 注意事项:size() length() 的计算不包含 \0

解释:它不包含最后作为结尾标识符的 \0,告诉你的是有效的字符长度。

size()length() 的功能都是返回字符串有效长度,功能上没有区别。

解释: 这是一个 "历史包袱" 问题。

因为 string 比 STL 出现的还要早一些,所以有了STL容器之后就习惯地给出了 size() 。

 length() 是代替传统的C字符串,所以针对C中的 strlen ,给出相应的函数 length() 。

C++ 中 string 成员函数 length() 等同于 size() ,功能没有任何区别!没错,就是没有任何区别。

" 不信?Talk is cheap,show me the code!"

不信可以看 C++ 标准库中的 string 中 size() 和 length() 的源代码:

size_type   __CLR_OR_THIS_CALL   length()   const
{   //   return   length   of   sequence
    return   (_Mysize);
}

size_type   __CLR_OR_THIS_CALL   size()   const
{   //   return   length   of   sequence
    return   (_Mysize);
} 

看源代码说话,这实现的都一模一样,return (_Mysize) 甚至连注释都一样。

0x01 返回字符串最大长度的 max_size

 返回字符串可以达到的最大长度。

💬 我们来试试:

int main(void)
{
	string s1;
	cout << s1.max_size() << endl;

	return 0;
}

  long long int 最大值  2^{63}-1

🧀 小芝士:C++string ax_size():返回字符串最大长度 (4294967294)  2^32=4294967296,其中有两位系统做什么了?

-1被占用为不存在的位置string::npos,因此不能用于长度(2^32-1)。字符串最后一字节保留给\0,因此只能再少一字节(2^32-2)。

0x02 返回空间总大小的 capacity()

容量,代表能存多少个有效字符。

💬 代码演示:

int main(void)
{
	string s1("abcdef");
	cout << s1.capacity() << endl;

	return 0;
}

❓ 这里容量为什么是 15?

为了优化内存使用,string 可能会使用一个小的内部缓冲区来存储短字符串,我们称为 短字符串优化 SBO (Small Buffer Optimization) 。这个内部缓冲区的大小通常是实现特定的,但常见的大小是 15 个字符,所以这里的返回值是 15。

值得注意的是,这个内部缓冲区的大小可以根据不同的标准库实现而异,因此并不是所有 C++ 标准库都会返回 15。此外,当你向字符串添加更多字符时,标准库会动态地重新分配内存,以适应更多的字符,所以 capacity() 的返回值可能会在运行时改变。

SBO 是"Small Buffer Optimization" 的缩写,意为"小缓冲区优化"。它是一种优化策略,常常用于实现动态字符串(如string)或其他容器,以提高性能和减少内存分配的开销。

SBO的基本思想是,如果字符串或容器的内容比较小(通常是一个较小的固定大小),那么就不需要分配额外的内存来存储内容,而是将内容存储在对象的内部缓冲区中。这可以减少内存分配和释放的次数,从而提高性能。

0x03 清空有效字符的 clear()

📚 clear() 用于清空有效数据

int main(void)
{
	string s1("abcdef");
	cout << "清空前: " << s1 << endl;
	
	s1.clear();
	cout << "清空后: " << s1 << endl;

	return 0;
}

值得注意的是,clear 只是把数据清了,但是容量还在:

int main(void)
{
	string s1("abcdef");
	cout << "清空前: " << s1 << endl;
	cout << s1.capacity() << endl;

	
	s1.clear();
	cout << "清空后: " << s1 << endl;
	cout << s1.capacity() << endl;

	return 0;
}

0x04 开完空间就跑路的 reserve() 和开完空间还给你初始化的 resize()

 先说 reserve,这个单词的拼写和 reverse 有点像,但是注意分别!

reserve:保留    reverse:逆置)

📚 reserve 会改变容量

.reserve(size_t res_arg=0)
为string预留空间,不改变有效元素个数,当reserve的参数小于
string的底层空间总大小时,reserve不会改变容量大小。

s.reserve(1000);    // 请求申请至少能存储1000个数据的容量

 我们再来说说 resize

reserve 开空间,影响容量。而 resize 是开空间,并对这些空间给一个初始值,进行初始化。

(它们的区别类似于 malloc 和 calloc)

 举个形象地例子 ——

" reserveresize 都是卖房子的(开空间的),reserve 只是把房子卖给你(开空间),而 resize 是一条龙服务,房子卖给你(开空间)之后还帮你搞装修(初始化),你还可以指定装修风格(初始化内容),如果不指定会按默认的简约风格装修(不指定默认用 \0 初始化)"

💬 看代码:

void TestPushBack() {
	string s1;
	s1.reserve(100);

	string s2;
	s2.resize(100);
}

💬 指定 resize 初始化字符:

string s2;
s2.resize(100, 'x');   // 给值,指定初始化字符

💬 如果 resize 的大小比你当前的数据还要小:

string s5("hello world");  
s4.resize(5); 

resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字 符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的 元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大 小,如果是将元素个数减少,底层空间总大小不变

Ⅳ. string类对象的访问及遍历操作

函数名称功能说明
operator[]返回pos位置的字符,const string类对象调用
begin + endbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
rbegin + rendrbegin获取一个字符的反向迭代器 + rend获取最后一个字符下一个位置的迭代器
范围 forC++11支持更简洁的范围 for 新遍历方式

0x00 访问字符串字符的 operator[] 和 at()

如果访问这个字符串的每一个字符: 

int main(void)
{
	string s1("hello world");
	for (size_t i = 0; i < s1.size(); i++) {
		cout << s1[i] << " ";
	}
	cout << endl;

	return 0;
}

 这里可以和数组一样使用 for 循环遍历,然后通过 s1[i] 来获取每个字符。

我们熟知的数组,可以用 "方括号" 直接解引用,即数组的第 i 个位置解引用。

这里很像,只是在 string 这里它作为函数调用,这就是 operator[]  ,并且等价于:

cout << s1[i] << " ";        
cout << s1.operator[](i) << " ";   // 取字符串第i个位置的字符的引用

💬 我们不仅能读,还可以写。比如我们给每个字符 +1:

int main(void)
{
	string s1("hello world");

	for (size_t i = 0; i < s1.size(); i++) {
		s1[i] += 1;  // 对s1每个位置的字符都+1
	}
	cout << s1 << endl;

	return 0;
}

 🤔 思考:operator[] 可以写的原因是什么?

❓ string 把这里设计成引用的目的是什么?operator[] 的底层是这样设计的:

char& operator[] (size_t pos)
{
    return _str[pos];
}

这里的 & 不是为了减少拷贝而使用的,因为就算拷贝一个 char 代价也不大。

 这里 operator[] 使用引用返回,是为了能够支持修改返回的变量!

顺便一提,除了 operator[] 还有一个 at() ,也是早期支持的一个接口。

💬 代码演示:它和 operator[] 用处是一样的,at() 是像函数一样去使用的

int main(void)
{
	string s1("hello world");

	for (size_t i = 0; i < s1.size(); i++) {
		s1.at(i) += 1;
	}
	cout << s1 << endl;

	return 0;
}

at()operator[] 的区别 —— 它们检查越界的方式不一样。

 operator[] 是使用断言处理的,断言是一种激进的处理手段。

char& operator[] (size_t pos)
{
    assert(pos < _size);
    return _str[pos];
}

at() 是比较温柔的处理方式,如果 pos >= size 就 throw 一个异常对象出去。

但是一般情况还是 operator[] 用的比较多一些,at() 用的少。

0x01 初识迭代器

"大哥来了……"

迭代器 是 STL 六大组件之一,是用来访问和修改容器的。

💬 用一下试试:

int main(void)
{
	string s1("hello");
	
    // 迭代器
	string::iterator it = s1.begin();
	while (it != s1.end()) {
		cout << *it << " ";
		it++;
	}
	cout << endl;

	return 0;
}

注意,这是本教程第一次提到迭代器,后续我会详细讲解。

如果你是第一次接触 "迭代器的概念",不妨可以先把迭代器想象成 "像指针一样的类型"。

 (可以试着想象遍历的过程)

迭代器遍历 的意义是什么呢?

对于 string,无论是正着遍历,倒着遍历,下标 + [] 都足够好用,为什么还要迭代器呢?

当然,对于 string,下标和 [] 确实足够好用,我们在学习 C 语言的时候就先入为主地使用了,

确实可以不用迭代器。但是如果是其他容器(数据结构)呢?

比如 list、map / set  不支持 下标 + [] 遍历,迭代器就排上用场了,这就是迭代器存在的意义。

迭代器是通用的遍历方式。

🔺 总结: 对于 string,你得会用迭代器,但是一般我们还是喜欢用 下标 + [] 遍历

迭代器有很多,此外还有反向迭代器、const 迭代器……

这些都可以通过看文档去了解和学习。对于迭代器我们下一章还会详细讲解,

(我们今天主要说的还是 string,所以这里要讲究点到为止)

0x02 甜甜的语法糖 —— 范围for

还有一种遍历字符串的方式,范围 for

这个我们在讲 auto 关键字的时候讲过了,它是一个用起来是很甜的语法糖。

💬 用起来非常爽,不用加加也不用解引用:

int main(void)
{
	string s1("hello");

	for (auto e : s1) {
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

如此完美,当然不仅仅是能读,还能写 ——

int main(void)
{
	string s1("hello");

	for (auto& e : s1) {    // 引用修改
		e += 1;
	}
	cout << s1 << endl;

	return 0;
}

 ​​​​​​

(这个 ifmmp 就是对 hello 这个单词的简单加密,你们学不学密码学?)

这里加引用是为了可以修改。不仅如此,遇到数据大的情况下也会有加 & 的时候(减少拷贝)。

其实,你不用 auto 也可以用返回 for

for (char& e : s1) {
	e += 1;
}
cout << s1 << endl;

 我们习惯用 auto ,是因为 C++11 改版之后的 auto 可以自动推导类型,

让这个语法糖更特么香了!复习猛戳 👉【C++要笑着学】范围for    (位于Ⅲ处 0x00 ~ 0x02)

再说一嘴:这个范围 for 虽然看起来和听上去都很

又是自动迭代又是自动判断结束的,

但其实它底层也就是编译器在编译后把这段代码替换成了迭代器而已。

(下一章讲 string 模拟实现的时候会顺便带大家演示一下范围for的实现原理)

Ⅴ. string 类对象的修改操作

函数名称功能说明
push_back在字符串后尾插字符c
append在字符后追加一个字符串
operator+=    (重点)在字符串后追加字符串str
c_str              (重点)返回c格式字符串
find  +  npos  (重点)从字符串pos位置开始往后找字符c,返回该字符自字符串中的位置
rfind从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
substr在str中从pos位置开始,截取n个字符,然后将其返回

0x00 用来插入字符的 push_back() 

string 用的最多的可能就是 push_back() 插入了

📚 push_back:插入一个字符,但只能插入一个字符,不能插入字符串。

int main(void)
{
	string s1("hello, ");
	s1.push_back('C');
	cout << s1 << endl;

	return 0;
}

0x01 用来插入字符串的 append()

📚 append() 可以插入字符串,它就能干很多 

(关于迭代器部分的知识我下面几章会说)

💬 这里面用的最多的就是 (1) 和 (3) ,插入一个完整字符串和插入一个对象。

int main(void)
{
	string s1("hello, ");
	s1.append("C++");
	cout << s1 << endl;

	return 0;
}

0x02 用起来爽到飞起的 operator+=  

"没关系,operator+= 会出手,字符和字符串串通吃!!"

 📚 还有用起来更舒服的 —— operator+=

int main(void)
{
	string s1("hello, ");
	s1 += 'C';    // 加个字符
	cout << s1 << endl;

	s1 += ' ';    // 加个空格
	cout << s1 << endl;

	s1 += "C++";  // 加字符串
	cout << s1 << endl;

	return 0;
}

 妈的这也太爽了。

0x03 返回C格式字符串的 c_str

📚 c_str() 用来返回字符串指针

💬 代码演示:

void Test() {
	string s1("hello string");
	cout << s1 << endl;  // 调用重载的流插入运算符打印的
	cout << s1.c_str() << endl;  // 这是调字符串打印的,c_str 是遇到 \0 结束的,
}

 (结果看起来很像)

❓ 那这个 c_str 有什么意义呢?

"存在即合理"

// 获取file后缀
void Test() {
	string file("test.txt"); 
	FILE* pf = fopen(file.c_str(), "w");  
}

比如这里需要打开文件,fopen 第一个参数要求是 const char*

所以这里怎么能直接放 string 是不行的,这时候可以用 c_str()  就可以把字符串的地址返回出来。

0x04 追查字符位置的 find() 和砍字符串的 substr()

如果我们要取像这样的文件后缀名 .txt  或  .cpp,该怎么取?使用 find() 就可以很好地解决!

📚 find:从字符串 pos 位置开始往后找字符 c ,返回该字符在字符串中的位置。

string file("test.txt");
FILE* pf = fopen(file.c_str(), "w");

size_t pos = file.find('.');  // 不写第二个参数,默认从0位置开始找。

通过文档,了解它的返回值后,我们就知道如何判断它了。

在这之前我们还要介绍一下可以砍字符串的 substr

📚 substr:在 str 中从 pos 位置开始,截取 n 个字符,然后将其返回。

💬 判断部分:

void Test() {
	string file("test.txt");
	FILE* pf = fopen(file.c_str(), "w");

	size_t pos = file.find('.');  // 不写第二个参数,默认从0位置开始找。
	if (pos != string::npos) {
		string suffix = file.substr(pos, file.size() - pos);
		cout << suffix << endl;
	}
}

find 截取到 " . " 存入 pos,pos 的位置就是起始位置,就作为 substr() 的第一个参数 pos,

substr() 的第二个参数 len ,我们可以通过 size() - pos 来算出要截取多少个字符。

 其实第二个参数你可以不给,看上面的文档可知,参数二 len 的缺省值是 npos,

并且 len 是 size_t 的,也就是说如果你不写,它默认会是我们所说的 "上亿的大数"

这种数肯定会超出字符串长度。而且要求是获取后缀,

因为考虑到 .txt  .cpp 这样的后缀,后面也没东西,这时候就会有多少出多少

💬 所以如果你懒的话甚至可以直接这样写:

void Test() {
	string file("test.txt");
	FILE* pf = fopen(file.c_str(), "w");

	size_t pos = file.find('.');
	if (pos != string::npos) {
		string suffix = file.substr(pos);  // 给个pos就完事了
		cout << suffix << endl;
	}
}

find 是很常用的!也是很重要的!我们还可以拿它取 url :

void Test() {
	// 创建字符串记录网址
	string url("http://www.cplusplus.com/reference/string/string/find/");

	// 取出协议
	size_t pos1 = url.find(':'); 
	string protocol = url.substr(0, pos1 - 0);
	cout << protocol << endl; 

	// 取出域名
	size_t pos2 = url.find('/', pos1 + 3);  // 冒号位置+3开始往后找(w位置) 
	string domain = url.substr(pos1 + 3, pos2 - (pos1 + 3));
	cout << domain << endl;

	// 取出路径
	string uri = url.substr(pos2 + 1);
	cout << uri << endl;
}

0x05 反向追查字符串的 rfind()  

但是,如果文件后缀名是 .txt.zip 呢?我们要取 .zip 啊。

 如果还是按刚才的方式去取,会从第一个 " . " 开始选取·:

void Test() {
	string file("test.txt.zip");
	FILE* pf = fopen(file.c_str(), "w");

	size_t pos = file.find('.');
	if (pos != string::npos) {
		string suffix = file.substr(pos, file.size() - pos);
		cout << suffix << endl;
	}
}

 (最后得到的结果不是我们想要的)

这种情况,就需要从右往左去找了。

📚 rfind:从字符串 pos 位置开始往前找字符 c ,返回该字符在字符串中的位置。

rfind,就是 right find —— 从右往左去找字符。

💬 这里我们只需要改成 rfind 就可以了:

void Test() {
	string file("test.txt.zip");
	FILE* pf = fopen(file.c_str(), "w");

	size_t pos = file.rfind('.');
	if (pos != string::npos) {
		string suffix = file.substr(pos, file.size() - pos);
		cout << suffix << endl;
	}
}

Ⅵ.  string 类非成员函数

函数功能说明
operator+尽量少用,因为传值返回,导致深拷贝效率低
operator>>   (重点)输入运算符重载
operator<<   (重点)输出运算符重载
getline          (重点)获取一行字符串
relational operators  (重点)大小比较

string 类中还有很多操作,这里就不一一列举了,需要用到时不明白可以查文档。

……

​​

📌 [ 笔者 ]   王亦优
📃 [ 更新 ]   2022.4.27 | 2023.10.4(重制)
❌ [ 勘误 ]   暂无
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

📜 参考资料 

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. C++[EB/OL]. 2021[2021.8.31]

solotony. 百度知道[EB/OL]. []. https://zhidao.baidu.com/question/1110123513762253459.html.

评论 28
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王平渊

喜欢的话可以支持下我的付费专栏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值