Ⅰ. string 的前置知识
01 学习 string 类的原因
C 语言中的字符串,是以 \0 结尾的一些字符的集合。为了方便操作,C标准库提供了一些 str 系列的库函数。
但这些库函数与字符串是分离的,不太符合面向对象的思想。而且底层空间需用户自己管理。
在工作中为了方便大多使用 string 类。
02 认识 string 类
简单来说,string 就是一个管理字符串的类。
① 字符串是表示字符序列的类。
② 标准的字符串提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符串的设计特性。
③ string 类是使用char,作为它的字符类型,使用它的默认 char_traits 和分配器类型。
④ string 类是 basic_string 模板类的一个实例,它使用 char 来实例化 basic_string 模板类,并用 char_traits 和 allocator 作为 basic_string 的默认参数。
⑤ 这个类独立于所使用的编码来处理字节。 如果用来处理多字节或变长字符(如UTF-8)的系列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
① string 是表示字符串的字符串类。
② 该类的接口与常规容器的接口基本相同,又添加了一些专门用来操作字符串的常规操作。
③ string 在底层上实际是 basic_string 模板类的别名:
typedef basic_string<char, char_traits, allocator> string;
④ 不能操作多字节或边长字符的序列。
03 basic_string 模板类
从文档看出,string 的原生类并不是直接定义了一个 string 类,而是定义出了一个类模板。string 是用 typedef 出来的,其实是 basic_string<char>
我们先用 C 格式字符串构造一个 string 类对象:
#include <iostream>
using namespace std;
int main()
{
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 组成的,搞一个模板出来干什么?
这里和编码有关系。
04 编码表的由来
我们都知道,计算机最早是美国人发明的,所以早期在计算机上只需要显示英文。单英文的显示非常简单,ABCDEFG... 每个单英文组合就成为英文单词了,大写 + 小写,再加上一些标点符号,所以就出现了一套 ASCII 编码。
比如你输入了一个 a ,在计算机存储的话,由于计算机只有二进制,虽然 0 和1 只能表示两种状态,但多个 0 和1 一组合就可以表示很多状态。
为了记录这些字符,建立了一种映射关系,于是就产生了编码表:
比如我们要输入 hello ,在数组中存储:
char str1[] = "hello";
但在内存中,我们存储的并不是 hello,而是每个字母的 ASCII 码值。
之后我们打印的话,也是通过对照 ASCII 来输出这个数组。 这就是编码表。
但随着计算机的流行,使用的人越来越多,ASCII 编码表无法满足所有人的需求(并不是所有人都懂英文),这时就有人搞出了 Unicode。
Unicode‘ - 表示全世界文字的编码表
"统一码(Unicode),也叫万国码、单一码,是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。"
但中国文化博大精深,ASCII 编码表无法满足
UTF-8 常见的汉字都用2个字节去编码,生僻的用3个或4个
char str2[] = "吃饭";
存的时候存的是 UTF-8 对应的值,用的时候它会拿这个表去查。
如果对应不上,就会出现我们熟知的乱码。
我们已经知道,Unicode 是世界通用编码,而我们中国自己也有一套自己的编码方式。
GBK - 为中文量身定做的编码表
"GBK全称《汉字内码扩展规范》(GBK即“国标”、“扩展”汉语拼音的第一个字母,英文名称:Chinese Internal Code Specification)"
如果打开一个文本, Windows 一般默认编码是 GBK ,Linux 是 UTF-8 。
GBK 包括所有汉字,包括简体和繁体。 GB2312 只包括简体。
其实是,读音相同的字编到了一起:
int main()
{
char str2[] = "吃饭";
str2[1] += 1;
str2[3] += 1;
str2[1] += 1;
str2[3] += 1;
str2[1] += 1;
str2[3] += 1;
return 0;
}
下面我们回到 basic_string 模板类的讲解:
刚才介绍了编码,现在大家就能理解为什么 string 需要模板了
string 不仅有 char, 还有 wchar_t(2个字节),这就是 string 需要 basic_string 的原因
template<class T>
class basic_string {
private:
T* _str;
// ...
};
所以这个 T 不一定是 char ,还有可能是 wchar_t,这和编码有关
基于编码的原因,有些字符串就会用两个字符去表示一个字,所以就有了 wchar_t —— 宽字节。
05 其他字符编码的 string
宽字节是一种扩展的存储方式,Unicode 编码的字符一般以 wchar_t 类型存储
我们 sizeof(wchar_t)看看
#include <iostream>
using namespace std;
int main()
{
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*
还有 u16string(存16个比特位)、u32string(存32个比特位)
剩下的就不逐个介绍了
Ⅱ . string 类的常用接口
01 string 类对象的常见拷贝
需要重点掌握的:
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>
using namespace std;
void String_Test1()
{
/* 无参 */
string s1;
/* 带参 */
string s2("hello world");
/* 拷贝 */
string s3(s2);
}
#include <iostream>
#include<string>
using namespace std;
int main()
{
string s1("abcdef1234567");
string s2(s1, 2, 6);
cout << s2 << endl;
return 0;
}
仔细观察,我们发现l en 的缺省值给的是npos
也就是说,如果你不指定初始化多少个字符,它就会默认按 npos 个来
这个 npos 可以理解为是 string 里的一个静态变量,它是 -1
当没有指定 len 时,因为 npos 是 -1,而又是 size_t 无符号整型,长度将会是整型的最大值
也就是说,如果不传,它就相当于取整型最大值 2147783647
也就相当于取所有字符串了
代码演示:
int main()
{
string s1("abcdef1234567");
string s2(s1, 2);
cout << s2 << endl;
return 0;
}
Ⅲ . string 类对象的容量操作
01 size() 和 length()
先看代码:
int main()
{
string s1;
cin >> s1;
cout << "你输入了:" << s1 << endl;
return 0;
}
当我们想知道自己输入的字符串有多长时,难道靠直接数嘛?
这时候就能用 size() 或 length() 来解决
int main()
{
string s1;
cin >> s1;
cout << "你输入了:" << s1 << endl;
cout << s1.size() << endl;
cout << s1.length() << endl;
return 0;
}
这里的 size() 和 length() 的计算不包含 \0
告诉你的是字符的有效长度,这两个功能上没有区别
因为 string 比 STL 出现的还要早一些,所以有了STL容器之后就习惯地给出了 size() 。
而 length() 是代替传统的C字符串,所以针对C中的 strlen ,给出相应的函数 length() 。
C++ 中 string 成员函数 length() 等同于 size() ,功能没有任何区别!没错,就是没有任何区别
可以看 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);
}
02 返回字符串最大长度的 max_size
返回字符串可以达到的最大长度
int main()
{
string s1;
cout << s1.max_size() << endl;
return 0;
}
03 返回空间总大小的 capacity()
代表能存多少个有效字符
int main()
{
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的基本思想是,如果字符串或容器的内容比较小(通常是一个较小的固定大小),那么就不需要分配额外的内存来存储内容,而是将内容存储在对象的内部缓冲区中。这可以减少内存分配和释放的次数,从而提高性能。
04 清空有效字符的 clear()
int main()
{
string s1("abcdef");
cout << "清空前: " << s1 << endl;
s1.clear();
cout << "清空后: " << s1 << endl;
return 0;
}
clear 只是把数据清楚了,但容量还在:
int main()
{
string s1("abcdef");
cout << "清空前: " << s1 << endl;
cout << s1.capacity() << endl;
s1.clear();
cout << "清空后: " << s1 << endl;
cout << s1.capacity() << endl;
return 0;
}
05 开完空间跑路的 reserve() 和开完空间初始化的 resize()
reserve 会改变容量
.reserve(size_t res_arg=0)
为string预留空间,不改变有效元素个数,当reserve的参数小于
string的底层空间总大小时,reserve不会改变容量大小。
s.reserve(1000); //请求申请至少能存储1000个数据的容量
reserve 开空间,影响容量。resize 开空间,并对这些空间给一个初始值,进行初始化
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 类对象的访问及遍历操作
01 访问字符串字符的 operator[] 和 at()
如果要访问字符串的每一个字符:
int main()
{
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个位置的字符的引用
不仅可读,还可写:
int main()
{
string s1("hello world");
for (size_t i = 0; i < s1.size(); i++)
{
s1[i] += 1;
}
cout << s1 << endl;
return 0;
}
那么operator[] 可写的原因是什么呢?
因为它返回的是每个位置的字符的引用
那么 string 为什么要把这里设计成引用呢? operator[] 的底层是这样的:
char& operator[] (size_t pos)
{
return _str[pos];
}
这里的引用不是为了减少拷贝而使用的,而是为了可以支持修改返回的对象
at() 用处和 operator[] 一样:
int main()
{
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 就抛异常
但是一般情况还是 operator[] 用的比较多一些,at() 用的少
02 初始迭代器
迭代器是 STL 六大组件之一,是用来访问和修改容器的
使用一下:
int main()
{
string s1("hello");
// 迭代器
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
return 0;
}
第一次接触迭代器,我们可以把它想象成像指针一样的类型
既然可以使用下标 + [] 遍历了,那为什么还要迭代器呢?
对于 string,下标和 [] 确实足够好用,我们在学习 C 语言的时候就先入为主地使用了,
确实可以不用迭代器。但是如果是其他容器(数据结构)呢?
比如 list、map / set 不支持 下标 + [] 遍历,迭代器就排上用场了,这就是迭代器存在的意义。
迭代器是通用的遍历方式
总结:对于 string,你得会用迭代器,但是一般我们还是喜欢用 下标 + [] 遍历。
迭代器有很多,此外还有反向迭代器、const 迭代器……
这些都可以通过看文档去了解和学习
03 范围 for
还有一种遍历字符串的方式,范围 for
int main()
{
string s1("hello");
for (auto e : s1)
{
cout << e << " ";
}
cout << endl;
return 0;
}
不仅能读,还能写:
int main()
{
string s1("hello");
for (auto& e : s1)
{
e += 1;
}
cout << s1 << endl;
return 0;
}
这里加引用是为了可修改,不仅如此,在遇到数据大的情况下也会加 & (减少拷贝)
不用 auto 也可以使用范围 for
for (char& e : s1)
{
e += 1;
}
cout << s1 << endl;
习惯使用 auto,是因为 C++11 改版之后 auto 可以自动推导类型
范围 for 虽然看起来很厉害,
又是自动迭代又是自动判断结束的,
但其实它底层也就是编译器在编译后把这段代码替换成了迭代器而已。
Ⅴ . string 类对象的修改
01 用来插入字符的 push_back()
只能插入一个字符,不能插入字符串
int main()
{
string s1("hello ");
s1.push_back('w');
cout << s1 << endl;
return 0;
}
02 用来插入字符串的 append()
append() 可以插入字符串,可以干很多
用的最多的是(1)和(3)插入一个完整的字符串和插入一个对象
int main()
{
string s1("hello ");
s1.append("C++");
cout << s1 << endl;
return 0;
}
有没有一个东西可以既使用来插入字符,还可以使用去插入字符串的呢?
那就是 operator+=
03 operator+=
int main()
{
string s1("hello, ");
s1 += 'C'; // 加个字符
cout << s1 << endl;
s1 += ' '; // 加个空格
cout << s1 << endl;
s1 += "C++"; // 加字符串
cout << s1 << endl;
return 0;
}
04 返回 C 格式字符串的 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() 就可以把字符串的地址返回出来。
05 查找字符位置的 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位置开始找。
find 的返回值类型为无符号整型
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;
}
}
06 反向查找字符的 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() 就行:
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 类非成员函数
string 类中还有很多操作,这里就不一一列举了