目录
1.常用构成string类对象的3种方式:无参默认构造、用常量字符串构造、拷贝构造
(1)案例1:创建无参对象、带参对象,同时使用运算符重载函数遍历字符串对象并打印数据
3.1.观察在VS下,reserve扩容情况(注:reserve在VS下按1.5倍进行扩容)
3.2.观察在Linux下,reserve扩容情况(注:reserve在Linux下按2倍进行扩容)
3.3.在进行插入操作之前,使用reserve或者resize预先开辟空间可以减少在插入操作过程中频繁出现扩容导致性能下降问题。
(1)案例1:调用void resize (size_t n)开空间 + 剩余未使用空间初始化为初始化为缺失值'\0'。
(2)案例2:调用void resize (size_t n, char c)来开空间+指定剩余未使用空间初始化为固定值(字符c)
(3)案例3:调用void resize (size_t n)删除数据
string::find - C++ Reference (cplusplus.com)
(1)解析 std::string 中 find 函数返回 npos 表示查找失败无混淆的原因
(2)size_t find (char c, size_t pos = 0) const
6.2.(经典面试题)案例:把字符串"hello world i love you"的所有空格字符' '替换成字符串"%20"
(2)写法2:为了避免由于多次扩容(注:主要是异地扩容)导致代码性能下降问题,该写法就提前预先开辟空间来存放将来要存放的数据以此来减少扩容的次数。
9.1.string::swap成员函数 与 std::swap函数模板的对比
(2)string::swap 成员函数与 std::swap 函数模板的交换效率对比
(3) C++ 标准库已经提供一个通用的函数模板swap,但为什么string 类还要提供 swap 成员函数的原因。
string::c_str - C++ Reference (cplusplus.com)
1.1.iterator(正向) 、reverse_iterator(反向)类型的迭代器使用
(1)正向迭代器遍历访问:使用begin()、end(),迭起器正着遍历。
(2)反向迭代器遍历访问:使用rbegin()、rend(),让迭起器倒着遍历。
1.1.3.方式3:范围for(注:范围for的底层原理是迭代器)
1.2.const_iterator(正向)、const_reverse_iterator(反向)类型的迭代器使用
1.2.1.begin()和end()、rbegin()和rend()可以分别返回const正反向迭代器
(1)案例1:const正向迭代器——string类对象使用传引用传参且形参被const时,形参const string类对象的遍历需要使用const_iterator类型的迭代器。
(3)案例3:使用auto可以自动推导正反向const迭代器的类型
1.2.2.cbegin()和cend()、crbegin()和crend()只分别返回const正反向迭代器
2.at成员函数——char& at (size_t pos)、const char& at (size_t pos) const的使用
案例1:没有异常处理,at成员函数发生抛异常后会直接终止程序。
案例2:有异常处理,at成员函数发生抛异常后不会直接终止程序。
2.2.在实际当中我们很少使用at成员函数,而是经常使用operator[ ]的原因
一、string类(了解)
string - C++ 参考 (cplusplus.com)
1. 字符串是表示字符序列的类
2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
- string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;
- 不能操作多字节或者变长字符的序列。
在使用string类时,必须包含#include头文件以及using namespace std;
二、string类的常用接口说明
注意:下面我只讲解最常用的接口。
string类对象的常见构造
注:下面主要介绍如何使用构造函数来创建字符串对象。
string::string - C++ 参考 (cplusplus.com)

1.常用构成string类对象的3种方式:无参默认构造、用常量字符串构造、拷贝构造
#include<iostream>
#include<string>
using namespace std;
int main()
{
//1.创建string类对象常用的4种写法:
//1.1.方式1:调用无参构造函数string(),构造空的string类对象s1
string s1;//定义无参
//1.2.方式2:用常量字符串构造string类对象
//(1)写法1:调用string (const char* s)构造string类对象s2
string s2("hello world");//定义带参,使用const char*的常量字符串去初始化
//(2)写法2:单参构造函数支持隐式类型转换来构造string类对象s3。
//构造+拷贝构造 -> 优化为:构造
string s3 = "hello world";//单参构造函数支持隐式类型转换:即const char*的常量字符串隐式类型转换成
//string类对象后调用拷贝构造来创建字符串s3。
//解析:
//(1)表达式string s3 = "hello world"不是拷贝构造,而是类型转换。单参数构造函数支持隐式类型转换。
//表达式string s3 = "hello world"中的类型为const char*的"hello world"隐式类型转换为string类的对象。
//(2)构造+拷贝构造 -> 优化为:构造。即先传参数常量字符串"hello world"调用string (const char* s)构造函数
//创建string类的临时对象,然后再传参数临时对象调用string (const string& str)拷贝构造函数创建string类
//对象s3。
//注意:写法1和写法2都是用常量字符串构造string类对象,但是写法2是直接调用构造函数创建string类对象,
//而写法2是调用深拷贝的拷贝构造函数创建string类对象,使得写法2构造string类对象比写法1的效率要高,
//所以建议使用写法2 而不是 写法1.
//1.3.方式3:调用拷贝构造函数string (const string& str),拷贝构造string类对象s4
string s4(s2);
return 0;
}
(1)案例1:创建无参对象、带参对象,同时使用运算符重载函数遍历字符串对象并打印数据
使用string类的[ ]运算符重载函数依次访问字符串(对象)的每个字符并对每个字符进行++。同时用流插入、流提取重载函数打印修改后的字符串(对象)。
#include<iostream>
#include<string>
using namespace std;
int main()
{
//使用无参默认构造函数构造string类对象:
string s1;//定义无参对象
string s2("hello world");//定义带参,使用const char*的常量字符串去初始化
//像访问数组的方式去访问string类对象(注:这个是最简单的string类对象的访问方式)
for (size_t i = 0; i < s2.size(); ++i)
{
s2[i]++;
}
//对象整体打印:打印整个字符串
cout << s2 << endl;
//字符串的每个字符按一个一个进行打印
for (size_t i = 0; i < s2.size(); ++i)
{
cout << s2[i] << " ";
}
cout << endl;
return 0;
}

2.不常用的构造string类对象方式
#include<iostream>
#include<string>
using namespace std;
int main()
{
//常用构造string类对象的方式:
//1.调用默认构造函数string()
string s1;
cout << "s1:" << s1 << endl;
//2.调用带参构造函数string (const char* s)
string s2("hello world");
cout << "s2:" << s2 << endl;
//3.构造 + 拷贝string (const string& str) -> 优化为: 构造
string s3 = "hello world";
cout << "s3:" << s3 << endl;
//不常用的构造string类对象方式:
//4.调用子字符串构造函数string (const string& str, size_t pos, size_t len = npos)使得我们
//使用源字符串pos位置开始往后的len长度的字符串来构造string类对象。
//4.1.取一部分进行初始化
string s4(s3, 6, 3);
cout << "s4:" << s4 << endl;
//4.2.有多少取多少
string s5(s3, 6, 13);
cout << "s5:" << s5 << endl;
//4.3.使用缺省值npos作为第三个参数
string s6(s3, 6);
//(1)解析:若调用子字符串拷贝构造时不传第三个参数就默认调用缺省值npos作为第三个参数len的值。而size_t npos = -1
//即就是npos = 42亿多,所以当用npos作字符串成员函数中len(或 sublen)参数的值时,表示“直到字符串的末尾”。
//(2)结论:实际很少使用子字符串拷贝构造string (const string& str, size_t pos, size_t len = npos)
cout << "s6:" << s6 << endl;
//5.调用string (const char* s, size_t n)构造函数使得我们用字符串的前n个字符构造string类对象
string s7("hello world", 5);
cout << "s7:" << s7 << endl;
//6.调用string (size_t n, char c)
string s8(10, '*');//填充
cout << "s8:" << s8 << endl;
return 0;
}

string类对象的容量操作
注意:
- size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
- clear()只是将string中有效字符清空,不改变底层空间大小。
- resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
- reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
1.size、length、max_size
string::size - C++ Reference (cplusplus.com)
string::length - C++ Reference (cplusplus.com)
string::max_size - C++ Reference (cplusplus.com)
(1)size与length的差异:没有差异。因为这两个成员函数的功能都是测字符串长度的。
注意:我们平时使用size而不使用length,因为是为了与其他容器的接口保持一致。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1("hello world");
cout << s1.size() << endl;
cout << s1.length() << endl;//length字符串长度
//1.结论:从打印结果可以看出size和length的功能没有什么差异。平时我们只要使用size成员函数来计算数据结构的大小。
//2.由于size和length的功能差不多,但是为什么有了size后还要提供length呢?
//解答:string类是在STL模板之前就出现的而且一开始求string类的字符串长度的成员函数只有length,
//但是string类本质是个数据结构,而STL模板中的其他数据结构关于计算大小的成员函数是size,
//所以在string类纳入STL模板后为了使得string类的成员函数命名和STL模板中的其他数据结构的
//成员函数命名保持一致则此时我们需要为string类重新创建一个计算大小的成员函数size。
return 0;
}

(2)max_size
注意:max_size的作用是告诉我们这个字符串最大可以达到多长。由于max_size的计算结果是没有什么参考价值的,所以我们平时是不用max_size成员函数的。
2.capacity(容量)
string::capacity - C++ Reference (cplusplus.com)

注意:
- 在VS下认为capacity()成员函数的计算结果是不包含字符串终止符‘\0’的;即不认为‘\0’是有效字符,而认为字符串终止符‘\0’是个标识字符。
- capacity是用来计算字符串类的容量,但是这个容量是用来存放有效字符,而字符串终止符‘\0’不是有效字符,所以capacity计算的容量是不包括字符串终止符‘\0’的。但是实际上字符串类实际空间大小为capacity + 1,而这个1是用来存放字符串终止符'\0'的。

3.reserve(功能:开空间+不初始化)
注意:reserve只改变字符串对象成员变量capacity,而不改变成员变量size。
string::reserve - C++ Reference (cplusplus.com)

3.1.观察在VS下,reserve扩容情况(注:reserve在VS下按1.5倍进行扩容)
注意:下面代码是用来观察reserve在VS下的扩容情况,即观察reserve扩容倍数。
解析:插入函数push_back的内部会有扩容机制,所以我们通过不断调用push_back来尾插以此来观察push_back函数内部reserve的扩容倍数是多少。
#include<iostream>
#include<string>
using namespace std;
//下面程序的作用是帮助我们观察每次扩容的倍数
int main()
{
//观察扩容情况:按1.5倍进行扩容
string s;
size_t sz = s.capacity();//用sz表示当前容量
cout << "making s grow:\n";
cout << "capacity changed: " << sz << '\n';
for (int i = 0; i < 100; ++i)
{
s.push_back('c');//不断插入数据时会导致扩容
if (sz != s.capacity())//若当前容量sz 不等于成员函数capacity()获得的容量,则一定会进行扩容。
{
sz = s.capacity();//此时已经扩容,则就要更新当前容量sz的值
cout << "capacity changed: " << sz << '\n';
}
}
}

注意:由于capacity()的计算结果是不包含字符串终止符'\0'的,所以上面打印结果capacity = 15字节对应的实际空间是16字节,capacity = 31字节对应的实际空间是32字节,以此类推。
3.1.1.分析:不能从打印结果第2行capacity changed: 15、第3行capacity changed: 31,就认为在VS下reserve是按2倍进行扩容的。实际是从打印结果的第3行capacity changed: 31往后观察去判断reserve扩容倍数的实际情况。
(1)解析:一开始,string s代码是调用无参构造函数来构造空的string类对象s,使得此时string s存放的有效元素个数为0,使得string s存放的有效元素个数小于16,所以一开始string s存储字符的空间是静态数组_buf[16],此时capacity()计算的是静态数组_buf[16]的空间大小,所以第2行capacity changed: 15这个打印结果是计算静态数组_buf[16]的空间大小的而不是reserve扩容后结果。当后来不断对string s进行尾插后,string s存放的有效元素个数会不断增多直到有效元素个数大于16字节时,则此时string s存储字符的空间不再是静态数组_buf[16]而是指针char* _ptr指向的动态数组,而这个动态数组的空间是由reserve开空间得来的,所以要想观察reserve的扩容倍数应该从第3行capacity changed: 31开始往后对比进行判断reserve实际的扩容倍数。
(2)调试:
①当string s存放的有效字符个数size() <= capacity()时,string s字符存储位置一直是静态数组_buf[16]。(注意:静态数组的空间是在静态区的)
- 一开始,还没有开始尾插时,string s字符存储位置是静态数组_buf[16],调试结果如下:

- 尾插15个字符后,string s字符存储位置依然是静态数组_buf[16],调试结果如下:

②当string s存放的有效字符个数size() > capacity()时,string s字符存储位置不再是静态数组_buf[16],而是指针char* _ptr指向的动态数组,而且动态数组一开始的31字节是reserve开辟的。
- 尾插16个字符后,string s字符存储位置是_ptr指向的动态数组,调试结果如下:

(3)string底层的实际结构:
注意:当string类对象存放的数据个数少于16(即16字节)时,数据是存放在静态数组_buf中的,此时指针_ptr指向的动态数组空间为0字节;当string类对象存放的数据个数大于或等于16(即16字节)时,数据是存放在指针_ptr指向的动态数组中的。实际在内存当中string类对象是占用28字节的。

3.2.观察在Linux下,reserve扩容情况(注:reserve在Linux下按2倍进行扩容)
注意:在Linux下,string类对象是占8个字节。

3.3.在进行插入操作之前,使用reserve或者resize预先开辟空间可以减少在插入操作过程中频繁出现扩容导致性能下降问题。
①注意:利用reserve或者resize来提前预先开辟空间来存放插入的数据,以此来达到不扩容 或者 减少扩容的方法。
②reserve成员函数的应用场景:假设知道插入多少字符的情况下,调用成员函数reserve建议string类的空对象预先开辟足够大的空间来存放将来要插入的字符,进而使得我们后序避免进行扩容和避免扩容后可能存在浪费空间的问题。总的来说,reserve的作用是可以帮助我们提取开辟空间以此来减少扩容(注:主要进行异地扩容),进而避免扩容带来的性能消耗的问题(注:异地扩容会存在性能消耗,降低代码效率问题,因为异地扩容存在开新空间+拷贝数据+释放旧空间的问题)。
③减少扩容的代码
#include<iostream>
#include<string>
using namespace std;
int main()
{
//观察扩容情况 -- 1.5倍扩容
string s;
s.reserve(100);//假设我们知道要插入100个字符,则我们可以使用reserve成员函数建议
//string类空对象一开始预先动态开辟100个字节来存放后面我们要插入的100个字符,
//进而使得我们后序避免进行扩容,进而也避免扩容可能存在浪费空间的问题。
//注意:s.reserve(100)建议string类对象预先开辟100个字节空间,这个只是建议而不是一定要开辟100字节,
//但是预先开辟的字节空间一定不比100字节小,从打印结果可以看出s.reserve(100)使得string类对象
//预先开辟111个字节空间来存放后面插入的数据,进而避免进行扩容和扩容后可能存在浪费空间的问题。
size_t sz = s.capacity();
cout << "making s grow:\n";
cout << "capacity changed: " << sz << '\n';
for (int i = 0; i < 100; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
- VS编译器的打印结果:

- gcc编译器的打印结果:
注意:在gcc编译器下s.reserve(100)让string类的空对象一开始开辟100个字节空间。
④结论:若我们提前知道我们需要多少空间,则string类的成员函数reserve的作用是提前帮我们开辟空间,进而减少扩容(或者说是不扩容),进而提供效率。
4.resize(功能:开空间+初始化 或者 删除数据)
注意:resize会改变字符串对象的成员变量capacity和成员变量size的值。
string::resize - C++ Reference (cplusplus.com)

注意:reserve的作用是开空间但不会对空间进行初始化,而resize既开空间又对空间进行初始化。总的来说,reserve只会改变capacity的值而不会改变size的值,但是resize既改变capacity的值又改变size的值。总的来说,reserve的作用只有开空间(不初始化),但是resize的作用是开空间 + 初始化或者删除数据。
(1)案例1:调用void resize (size_t n)开空间 + 剩余未使用空间初始化为初始化为缺失值'\0'。

(2)案例2:调用void resize (size_t n, char c)来开空间+指定剩余未使用空间初始化为固定值(字符c)

(3)案例3:调用void resize (size_t n)删除数据
①注意:
- 若void resize (size_t n)中的参数n比string类对象size要大,则resize是进行扩容的;若void resize (size_t n)中的参数n比string类对象size要小,则resize是删除数据的,即resize会让string类对象保留前n个数据,然后把其他数据删除掉。
- 当使用resize来删除数据时是不会改变capacity的值的,只会改变size的值。
- 像顺序表这种连续内存空间的数据结构是不会轻易去缩容的。因为缩容是不支持原地缩容的,大多数缩容是异地缩容的(注:异地缩容的步骤包括开新空间+拷贝数据+删除旧空间),使得大多数缩容时会导致代码性能消耗大进而使得代码效率下降。所以我们一般在删除数据时是不考虑缩容的。
- 在string类中,不经常使用resize来扩容,而是使用reserve来扩容。但是对于vector是经常使用resize来扩容,而不是reserve来扩容,因为使用resize扩容可以初始化vector。
- C++的string类相比C语言的字符串最大的好处是:string类不用管理空间。
②代码:

string类对象的修改操作
注意:
- 在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
- 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
1.插入函数push_back、append
string::push_back - C++ Reference (cplusplus.com)
string::append - C++ Reference (cplusplus.com)


#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1("hello");
//一个字符一个字符插入:
s1.push_back(' ');//调用void push_back (char c),插入1个字符
s1.push_back('!');
cout << s1 << endl;
//直接插入一个字符串
s1.append("world");//经常使用的是string& append (const char* s)
cout << s1 << endl;
//总的来说,push_back是插入1个字符,而append是插入1个字符串。
return 0;
}

2.operator+=
string::operator+= - C++ Reference (cplusplus.com)
注意:
- 实际当中我们一般不使用push_back、append函数对string类对象进行插入字符/字符串,而是利用operator+=运算符重载函数完成插入。
- operator+=的底层原理是是调用push_back、append的,即operator+=函数体内部是重新把push_back、append函数封装起来。
- 结论:从此以后我们都是用operator+=函数来完成对string类对象的插入操作。
3.Insert
string::insert - C++ Reference (cplusplus.com)
(1)注意:
- Insert 成员函数是个插入函数。Insert 成员函数用于在字符串指定位置插入字符或字符串,使得Insert 可以实现头插、尾插功能,从而在一定程度上可以代替push_back(尾插 1 个字符)和 append(尾插常量字符串 或 string 类对象)。但在实际使用中,并不建议用 Insert 来替代 push_back 和 append 进行常规的尾插操作。因为Insert 函数在插入数据时,通常需要挪动插入位置之后的数据,这在数据量较大时会带来较高的性能开销,导致效率低下。特别是对于尾插操作,push_back 和 append 通常有针对尾端插入的优化,它们直接在字符串的末尾添加数据,不需要挪动其他数据,效率更高。
- 尽量少用 insert 函数,因为其在使用过程中常常存在需大量挪动数据的情况,这会导致代码效率变得低下。
(2)案例:利用Insert成员函数在字符串任意位置插入数据
代码:
#include<string>
#include<iostream>
using namespace std;
int main()
{
//insert/erase不推荐经常使用,能少用就少用
//因为他们可能都存在要挪动数据,效率低下
string s1("world");
//头插
s1.insert(0, "hello");//在头部插入一个字符串
cout << s1 << endl;
//在字符串对象s1的第5个位置插入一个空格字符的3种方式:
//方式1:调用string& insert (size_t pos, size_t n, char c),
//在pos位置插入n个字符c。
s1.insert(5, 1, ' ');
cout << s1 << endl;
//方式2:调用 string& insert (size_t pos, const char* s),
//在pos位置插入指针s指向的字符串。
/*s1.insert(5, " ");
cout << s1 << endl;*/
//方式3:调用iterator insert (iterator p, char c),在迭起器(指针)p指向位置
//插入字符c。
//注:s1.begin()返回的是指向字符串s1第一个位置的迭起。虽然在vs中迭代器不是指针,
//但是在iterator insert(iterator p, char c)函数中可以把迭代器iterator p当成一个
//指针,进而使得我们可以使用s1.begin()+5来确定字符串第5个位置的字符。
/*s1.insert(s1.begin()+5, ' ');
cout << s1 << endl;*/
return 0;
}
测试:
4.erase
注意:Insert 和 erase 不推荐经常使用,能少用就少用,因为它们是需要挪动数据去完成插入 、删除数据的,效率低下。
string::erase - C++ Reference (cplusplus.com)

注意:在使用 string 类的 erase 成员函数时,通常不建议频繁使用。因为当使用 erase 函数删除字符串中间或头部位置的数据时,后续的数据需要向前挪动来填补被删除数据留下的空位,这个挪动数据的过程会消耗一定的时间和计算资源,进而导致代码的执行效率变低。而如果是删除字符串末尾的数据,相对来说对整体效率的影响一般会小一些,但总体来说,考虑到其删除中间或头部数据时存在影响效率的挪动操作,还是尽量避免过于频繁地使用它。
4.1.案例
(1)案例1:删除单个字符
#include<string>
#include<iostream>
using namespace std;
int main()
{
string s2("hello world");
cout << s2 << endl;
//案例1:删除字符串"hello world"第5个位置空格字符' '的2种方式:
//方式1:调用string& erase (size_t pos = 0, size_t len = npos),
//从第pos位置开始删除len个字符
//s2.erase(5, 1);
//方式2:调用iterator erase (iterator p),删除迭起器(指针)p指向的字符
s2.erase(s2.begin() + 5);
cout << s2 << endl;
return 0;
}
(2)案例2:删除字符串(即删除多个字符)
#include<string>
#include<iostream>
using namespace std;
int main()
{
string s2("hello world");
cout << s2 << endl;
//案例2:删除字符串"hello world"中的字符串" world":方式:
//解析:string& erase (size_t pos = 0, size_t len = npos),从第pos位置
//开始删除len个字符。若没有传参给len,则len默认使用缺省值pos。
s2.erase(5, 6);
//s2.erase(5, 30);//由于len = 30比从第5个位置开始往后的字符串长度要长,则能删多少就删多少。
//s2.erase(5);//参数len使用默认缺省值npos
cout << s2 << endl;
return 0;
}
5.replace
string::replace - C++ Reference (cplusplus.com)

注意:对于 string 类的 replace 成员函数,建议尽量少用。因为在使用 replace 函数时,若替换后字符串长度超出当前容量,可能会触发扩容操作(存在异地扩容情况),并且在替换数据之前,若涉及中间位置的数据替换,往往需要挪动相关数据,这两种情况都会产生额外开销,使得代码效率降低。不过在数据量较小等特定场景下,可根据实际需求合理使用该函数。
(1)案例:使用replace函数把字符串"hello world"中的所有空格替换成%%d
①方法1:
②方法2:
分析不推荐使用replace的原因:
- 空间扩容问题:当要替换的字符串长度大于原字符串中被替换部分的长度时,如果原字符串的容量不足以容纳替换后的结果,就需要进行扩容操作。扩容操作会涉及到重新分配内存、复制数据等,这会带来一定的性能开销。
- 数据挪动问题:在进行替换时,即使不需要扩容,也需要将被替换位置之后的字符进行挪动,以腾出空间给新的字符串,这也会消耗一定的时间和计算资源。
6.find
string::find - C++ Reference (cplusplus.com)

6.1.对find成员函数的介绍
(1)解析 std::string 中 find 函数返回 npos 表示查找失败无混淆的原因
注意:下面将针对find函数在查找失败时,为何敢于返回npos,且不担心真的在npos位置找到目标字符而导致混淆的情况展开分析。
在std::string中,find函数返回npos表示查找失败,且不会与在有效位置找到目标字符混淆,原因如下:
size_t类型特性:size_t是无符号整数类型,用于表示大小或计数。其可表示范围从 0 到SIZE_MAX。string类的下标为size_t类型,这意味着npos(作为size_t的最大值)必然大于任何有效的字符串索引。npos的定义:npos是string类的静态常量成员,值等于size_t的最大值(如在 32 位系统中通常为 4294967295)。由于字符串索引从 0 开始,实际字符串长度远小于npos,所以当find函数返回npos,能明确这代表查找失败,而非有效索引。find函数行为逻辑:find函数查找失败时返回npos,成功时返回子字符串起始位置。其返回值仅有有效索引和npos两种情况。若返回npos,就表明在字符串中未找到目标子字符串。- 不存在在
npos位置找到字符的可能:npos大于任何实际字符串长度,即便在极端假设下字符串长度为npos,因索引从 0 开始,find函数也不可能在npos位置找到字符。
综上,find函数返回npos来表示查找失败不会产生混淆,实际使用find函数时,通过检查返回值是否等于npos,就能判断查找是否成功。
(2)size_t find (char c, size_t pos = 0) const
std::string类的成员函数size_t find (char c, size_t pos = 0) const用于在当前字符串对象中查找单个字符c,并从指定的位置pos开始搜索。以下是该函数的详细描述:
①功能描述
find函数在当前字符串对象中从位置pos开始查找字符c。- 如果
pos超出了当前字符串的长度,函数将返回std::string::npos,表示没有找到字符。 - 如果字符
c在字符串中出现多次,函数将返回第一个匹配项的索引。
②参数说明
char c:这是要查找的单个字符。size_t pos = 0:这是开始搜索的字符位置索引。默认值为0,表示从字符串的开始位置进行搜索。pos的类型是size_t,它是一个无符号整数类型,通常用于表示大小或计数。
③返回值
size_t:如果找到了字符c,则返回字符c在字符串中的起始位置(索引)。- 如果没有找到字符
c,则返回std::string::npos。std::string::npos是一个特殊的常量,等于size_t类型的最大可能值,通常用来表示未找到任何匹配项。
6.2.(经典面试题)案例:把字符串"hello world i love you"的所有空格字符' '替换成字符串"%20"
6.2.1.方法1:直接在源空间完成替换
思路:
- 首先,遍历源字符串
s1,统计其中空格字符的个数num,以便后续计算扩容所需空间大小。 - 然后,使用
reserve函数对源字符串s1进行扩容,扩容后的空间大小为s1.size() + 2 * num,原因是每个空格替换为"%20"后字符串长度会增加 2。 - 接着,利用
find函数从字符串开头查找空格字符,找到后使用replace函数将其替换为"%20",并更新下次查找的起始位置为pos + 3,继续循环查找并替换,直到find函数返回string::npos,表示找不到空格字符。 - 最后,输出替换后的字符串。
代码实现:
注意:
- 由于replace函数内部涉及扩容机制,所以我们可以提前开空间,避免replace替换时发生扩容,从而导致代码效率低下。
- 提前开空间来减少或者避免扩容的前提是我们知道要插入的数据个数,若不知数据个数可以利用计数思想来统计出来。
#include<string>
#include<iostream>
using namespace std;
//案例1:把字符串"hello world i love you"的所有空格字符' '替换成字符串"%20"。解决方式如下:
int main()
{
string s1("hello world i love you");
//从头到尾遍历源字符串s1,并统计源字符串s1中的空格字符' '的个数num,目的是方便下面计算源字符串要扩容的空间大小。
size_t num = 0;
for (auto ch : s1)
{
if (ch == ' ')
++num;
}
//解析:
//1.扩容有两种方式:一种是原地扩容,另一种是异地扩容。 而原地扩容是没有什么消耗,因为原地扩容只需要执行在原地扩大空间的操作即可;
//但是异地扩容是有性能消耗的,因为异地扩容是找一块足够大的内存先开辟一块新空间然后把源空间的内容拷贝到新空间中,然后销毁源空间,
//这一异地扩容的过程中存在多个操作(查找空间、拷贝数据、释放源空间),使得异地扩容的性能消耗远比原地扩容的性能消耗要大。总的来说,
//扩容是存在性能消耗的。
//2.在替换源字符串s1中的所有空格字符之前要先对源字符串的空间进行扩容的原因:
//repalc函数体内部是存在扩容机制的,因为replace函数功能是把源字符串中从指定位置开始的指定长度的子字符串 替换成 新字符串。
//但是在repalc函数执行替换之前是有检查源字符串空间是否充足并考虑是否要进行扩容的需要,所以repalc函数体内部存在扩容机制。
//但是扩容可能存在性能的消耗,异地扩容一定存在性能消耗,而原地扩容的性能消耗可以忽略不计。若进行频繁多次扩容则性能一定会被严重影响,
//所以我们必须要尽可能的减少扩容的次数,才能使得程序的性能得到提升。
//
//4.C++的new操作符的功能是不存在对源空间进行扩容操作的,即new操作符的功能只有申请空间(类似malloc) + 调用默认构造函数。
//所以C++要想对源空间进行扩容操作则必须调用reserve 或者 resize,reserve对扩大后没有使用的空间不进行初始化即reserve只能改变capacity而不能改变size的值,
//resize对扩大后没有使用的空间初始为空字符(即'\0')而且resize会改变capacity和size的值。总的来说,C++只有reserve 或者 resize有扩容的权限。
//
//3.减少扩容次数的方法:先预先开辟一块足够大且合适的内存空间来存放将来要存放的数据,则到要进行多次不同时的插入数据时就不会轻易发生扩容,
//这样我们就可以减少扩容的次数。
//
//4.提前开辟空间是用resize还是用reserve?
//解答: resize是对空间进行初始化而reserve不对空间进行初始化。由于replace通过挪动数据和替
//换数据会改变字符串成员变量size的值(即size的值是由replace来完成的),而resize会改变size和
//capacity的值,而reserve不能改变size只能改变capacity的值,所以replace函数对于size值的改
//变应是由replace来改变而不是用resize来改变,进而使得我们应该使用reserve来提取开辟空间。
//总的来说,挪动数据和替换数据会改变成员变量size的值,而size值的改变应由replace函数自己完
//成,所以这道题没有必要使用resize对空间进行初始化,所以这道题使用reserve来扩容。
//5.把源字符串的空间扩容成s1.size() + 2 * num的原因:
//(1)解析s1.size()是当前源字符串空间的大小,而2 * num是需要扩大空间的大小。
//(2)原因:由于每个空格字符' '替换成字符串"%20"后源字符串的长度就会增加2,所以由于源字符串一共有num个空格则源字符串所有
//空格字符' '替换成字符串"%20"后源字符串的长度就会增加2 * num,所以需要扩大的空间大小为2 * num。
s1.reserve(s1.size() + 2 * num);//扩容:提前开空间,避免repalce时扩容。
//调用size_t find (char c, size_t pos = 0) const函数,并默认从pos = 0位置开始在空格字符''。
size_t pos = s1.find(' ');
//注意:这里使用find函数失败后返回npos作为while循环的终止判断条件。
while (pos != string::npos)//size_t find (char c, size_t pos = 0) const。若find找到则
//返回这个字符第一次出现的位置;若没有找到则find函数失败后返
//回npos。
{
//调用string& replace (size_t pos, size_t len, const char* s);
s1.replace(pos, 1, "%20"); //替换
//每一次空格字符''替换成字符串"%20"都会使得源字符串的长度增加2,所以为了避免每次替换完后下一次都要
//在源字符串从头到尾找空格符'使得性能下降,则我们下一次找空格字符' '时必须从源字符串的pos + 3位置开始。
pos = s1.find(' ', pos + 3);
}
cout << s1 << endl;
return 0;
}
(1)方法1的实现过程及优化
①第1步:替换一个空格字符
②第2步:使用循环替换所有空格字符
分析:
- 遇到的问题:每次找到空格字符并完成替换后,下一次都要从头开始遍历字符串查找空格字符,导致效率低下。因为随着字符串长度的增加,这种重复的从头查找会浪费大量时间和计算资源。
- 优化思路:在每次替换完一个空格后,将下一次查找空格的起始位置更新为当前替换位置之后的某个位置(如下面第3步代码示例中的
pos + 3),这样可以避免不必要的从头查找,减少了重复遍历已经处理过的部分字符串,从而提高查找效率。注意:优化方案如下面第3步代码所示。
③第 3 步:使用循环替换所有空格字符 + 每一次替换完成后就修改下一次的查询位置
分析:
- 遇到的问题:
replace函数体内部存在扩容机制,当每次调用replace函数完成替换时,如果字符串对象的空间不够,replace函数体内部就会先进行扩容后才完成替换。而每一次扩容(尤其是异地扩容)都有不同程度的性能消耗,频繁的扩容操作会严重影响程序的整体性能。 - 优化思路:通过预先统计字符串中空格字符的数量,根据每个空格替换后字符串长度的增加量(每个空格替换为
"%20"后长度增加 2),计算出需要额外开辟的空间大小(即2 * num,num为空格数量),然后在进行替换操作之前,使用reserve函数提前为字符串开辟足够的空间。这样可以避免在replace函数执行过程中因空间不足而频繁进行扩容操作,从而减少了扩容带来的性能开销,提高了代码的整体效率。注意:优化方案如下面第4步代码所示。
④第4步:使用循环替换所有空格字符 + 每一次替换完成后就修改下一次的查询位置 + 统计所有空格字符的数量以便提前使用 reserve 来开辟空间来减少扩容的次数,进而提高代码效率
- 遇到的问题:虽然通过提前使用
reserve函数开辟空间减少了扩容次数,但从前往后查找空格字符并进行替换的方式,在数据挪动方面仍存在性能瓶颈。每次替换空格为"%20"时,都需要将后面的字符向后挪动,随着字符串长度增加和空格数量增多,这种数据挪动的开销会逐渐增大,影响整体性能。 - 优化方案:可以考虑从后往前查找空格字符并进行替换,同时在挪动数据之前精确计算所需空间大小并提前开辟,挪动数据时尽量一步到位。
-
注意:这种优化方案旨在进一步减少数据挪动带来的性能损耗,通过改变查找顺序和更精准的空间管理,提升字符串空格替换操作的整体性能。
-
从后往前查找替换 和 从前往后查找替换的对比:
- 从后往前替换
- 一步替换:在从后往前替换时,当遇到空格,直接将其替换为 “%20”,这是一个相对独立的操作,可看作一步完成替换。
- 移动字符逐渐变少:因为是从后往前处理,每次替换 “%20” 后,后续需要移动的字符数量就会减少。例如,对于字符串 “abc def ghi”,先处理最后的空格,将其替换为 “%20”,此时前面的 “abc def ghi” 不需要移动;接着处理倒数第二个空格,替换后,前面的 “abc def” 不需要移动,以此类推,随着替换的进行,需要移动的字符逐渐变少。
- 从前往后替换
- 非一步替换:从前往后替换时,每遇到一个空格,不仅要将空格替换为 “%20”,还需要将空格后面的所有字符都向后移动两位(因为 “%20” 占三个字符位置,原空格占一个字符位置)。例如,对于字符串 “abc def ghi”,当处理第一个空格时,将 “ ” 替换为 “%20”,同时 “def ghi” 都要向后移动两位;处理第二个空格时,“ghi” 要向后移动两位,所以它不是一步完成替换,而是涉及到替换和大量字符移动的复杂过程。
- 移动字符逐渐增多:从前往后替换时,每处理一个空格,后面需要移动的字符数量都会增加。因为每次替换后,后面的字符都要向后移动,随着处理的空格增多,需要移动的字符范围就越来越大。例如,对于字符串 “a b c”,处理第一个空格时,需要移动 “b c”;处理第二个空格时,需要移动 “c”,整体上移动字符的工作量是逐渐增加的。
- 从后往前替换
-
- 优化思路:
- 查找顺序优化:从后往前查找空格,在替换空格为
"%20"时,后面的字符不需要再进行多次挪动,因为后面的字符还未处理,这样可以减少数据挪动的次数和开销。例如,对于字符串"abc def ghi",如果从前往后替换,先将"abc "中的空格替换为"%20",后面的"def ghi"都需要向后挪动;而从后往前替换,先处理"ghi"前面的空格,此时"abc def "部分还未涉及挪动,减少了不必要的挪动操作。 - 空间计算与开辟:在开始替换操作之前,准确计算出整个字符串替换空格后最终的长度(包括原字符串长度和因空格替换增加的长度),然后一次性开辟足够的空间。这样在替换过程中,只需要按照从后往前的顺序将字符和替换后的字符串依次放置到新开辟的空间中即可,避免了因空间不足导致的额外操作,进一步提高效率。例如,统计出字符串中有
n个空格,原字符串长度为m,则最终需要的空间大小为m + 2 * n,提前开辟这么大的空间,然后从后往前进行替换和放置字符。
- 查找顺序优化:从后往前查找空格,在替换空格为
- 优化代码3种写法(注意:下面3种写法主要是为了处理下标 = 0位置遇到空格字符这种极端情况):
- 写法1(建议采用该种写法)
#include<string> #include<iostream> using namespace std; int main() { string s1(" hello world i love you"); cout << "原始字符串: " << s1 << endl; // 统计源字符串s1中所有空格字符' '的个数num size_t num = 0; for (auto ch : s1) { if (ch == ' ') { ++num; } } // 记录原字符串末尾位置的后一个位置 // 解释:由于后续要进行resize操作会改变string对象内部表示大小相关的状态(涉及私有成员变量_size等的改变), // 所以需要先基于原始字符串长度记录原字符串末尾位置的后一个位置,以便后续从后往前遍历替换操作能准确知晓原字符串的情况。 size_t oldEnd = s1.size(); // 记录新字符串末尾位置 // 计算方式为原字符串长度加上因替换空格而增加的长度(每个空格替换为"%20",长度增加2),同样需在resize之前记录, // 方便后续从后往前替换时作为新字符串的索引参考。 size_t newEnd = s1.size() + 2 * num - 1; // 提前开辟空间,避免replace进行替换时由于空间不够进行扩容操作 // 注意:由于是从后往前查找替换,我们需要预先为替换后的字符串开辟足够空间并且更新string对象的_size属性, // 以匹配最终替换完成后的字符串长度,这样才能保证后续通过operator[]重载函数访问和修改字符时不会越界访问, // 进而避免发生断言报错。而resize函数恰好能满足既开辟空间又改变_size值的需求,相比之下,reserve只改变容量不改变_size, // 在此场景下不符合要求,所以此处使用resize来开空间并初始化新开辟的空间(此处用默认初始化,若有需要也可指定初始化字符)。 s1.resize(s1.size() + 2 * num); // 从后往前遍历 size_t i = 0; for (i = oldEnd; i > 0; --i) { if (s1[i - 1] != ' ') { // 注意:由于operator[]重载函数中存在对插入位置pos的合法性进行断言assert(pos < _size),所以在修改s1[newEnd--]位置的字符 // 之前必须确保newEnd < _size,这里newEnd随着替换操作递减,其初始值以及整个递减过程都应保证在合法的_size范围内, // 否则会发生越界访问进而引发operator[]重载函数的断言报错,导致程序异常终止。 s1[newEnd--] = s1[i - 1]; } else { // 替换思路1: // 通过逐个字符赋值的方式将空格替换为"%20",按照从后往前的顺序依次赋值到对应位置, // 这种方式较为直观地实现了替换逻辑,且能很好地配合从后往前的整体替换流程。 //s1[newEnd--] = '0'; //s1[newEnd--] = '2'; //s1[newEnd--] = '%'; // 替换思路2:使用replace函数进行替换 // 也可以使用replace函数进行替换,其语义更清晰地表示了从指定位置开始替换指定长度的子字符串为新字符串的操作, // 此处从newEnd - 2位置开始替换3个字符为"%20",然后相应地更新newEnd的值,使其指向替换后的下一个可用位置。 s1.replace(newEnd - 2, 3, "%20"); newEnd -= 3; } } cout << "替换后的字符串: " << s1 << endl; return 0; } - 写法2(该写法的代码有点冗余) 及 写法3(不建议采用该种写法)
#include<string> #include<iostream> using namespace std; int main() { string s1(" hello world i love you"); cout << "原始字符串: " << s1 << endl; size_t num = 0; for (auto ch : s1) { if (ch == ' ') { ++num; } } size_t oldEnd = s1.size() - 1; size_t newEnd = s1.size() + 2 * num - 1; s1.resize(s1.size() + 2 * num); size_t i = 0; //写法2: for (i = oldEnd; i > 0; --i) { if (s1[i] != ' ') { s1[newEnd--] = s1[i]; } else { // 替换思路1: s1[newEnd--] = '0'; s1[newEnd--] = '2'; s1[newEnd--] = '%'; //替换思路2: //s1.replace(newEnd - 2, 3, "%20"); //newEnd -= 3; } } if (i == 0 && s1[i] == ' ') { // 替换思路1: s1[newEnd--] = '0'; s1[newEnd--] = '2'; s1[newEnd--] = '%'; //替换思路2: //s1.replace(newEnd - 2, 3, "%20"); //newEnd -= 3; } //写法3: //for (i = oldEnd; i >= 0; --i) //{ // if (s1[i] != ' ') // { // s1[newEnd--] = s1[i]; // } // else // { // // 替换思路1: // s1[newEnd--] = '0'; // s1[newEnd--] = '2'; // s1[newEnd--] = '%'; // //替换思路2: // //s1.replace(newEnd - 2, 3, "%20"); // //newEnd -= 3; // } // if (i == 0) // break; //} cout << "替换后的字符串: " << s1 << endl; return 0; }
- 写法1(建议采用该种写法)
⑤总结:通过这样逐步分析问题并针对性地提出优化方案,可以不断改进代码的性能和效率,使其在处理字符串替换任务时更加高效和可靠。
6.2.2.方法2:开新空间协助完成旧空间的替换
- 思路:开新空间,遍历旧空间,遍历到非空格字符就尾插到新空间,而遍历到空格字符就尾插要替换的字符串到新空间,直到旧空间遍历结束。详细过程如下:
- 先预先开辟新空间,然后从头遍历源字符串当没有遇到空格字符时把源字符串的非空格字符拷贝到新空间中,当遇到空格字符时把字符串"%20"插入到新空间中,然后从源字符串当前空格字符的下一个字符开始往下遍历,重复之前的所有操作,直到遍历到源字符串的终止字符‘\0’才停止遍历。(注意:方法2不存在挪动数据,而方法1的replace存在挪动数据,所以方法2比方法1高效,但是方法2是以空间换取时间)
- 优点:方法2的优势在于不用挪动数据带来的程序性能下降的问题。
(1)写法1:不考虑多次扩容带来的代码性能下降的问题。
#include<string>
#include<iostream>
using namespace std;
int main()
{
string s1("hello world i love you");
cout << "s1:" << s1 << endl;
string newStr;//新空间
//注意:下面调用operator+=重载函数体内部也是存在扩容机制的,若是源字符串没有足够的空间,
//则多次调用operator+=就会存在多次扩容,而扩容(异地扩容)会导致代码性能下降的问题。
for (auto ch : s1)
{
if (ch != ' ')
newStr += ch;
else
newStr += "%20";
}
s1 = newStr;
cout << "newStr:" << newStr << endl;
cout << "s1:" << s1 << endl;
return 0;
}
(2)写法2:为了避免由于多次扩容(注:主要是异地扩容)导致代码性能下降问题,该写法就提前预先开辟空间来存放将来要存放的数据以此来减少扩容的次数。
时间复杂度O(N);空间复杂度O(N).
#include<string>
#include<iostream>
using namespace std;
int main()
{
string s1("hello world i love you");
cout << "s1:" << s1 << endl;
string newStr;//新空间
//统计源字符串s1中所有空格字符' '的个数num。
size_t num = 0;
for (auto ch : s1)
{
if (ch == ' ')
++num;
}
//提前预先开空间以此来避免调用operator+=运算符重载函数时发生扩容而引发
//代码性能下降的问题。
newStr.reserve(s1.size() + 2 * num);
//使用reserve函数来提前把源字符串空间进行扩容成合适空间以此来存放将来要插入的
//数据从而避免源字符串空间发生多次扩容。
for (auto ch : s1)
{
if (ch != ' ')
newStr += ch;
else
newStr += "%20";
}
s1 = newStr;
cout << "newStr:" << newStr << endl;
cout << "s1:" << s1 << endl;
return 0;
}
7.find_first_of、find_last_of
string::find_first_of - C++ Reference (cplusplus.com)
string::find_last_of - C++ Reference (cplusplus.com)


7.1.find_first_of
(1)函数功能:std::string 类的 find_first_of 成员函数用于在字符串中查找指定字符集合中任意一个字符首次出现的位置。
(2)函数原型
find_first_of 函数有多个重载版本,常见的两种原型如下:
//查找参数字符串中任意字符首次出现的位置
size_t find_first_of(const string& str, size_t pos = 0) const noexcept;
//查找字符数组中任意字符首次出现的位置
size_t find_first_of(const char* s, size_t pos = 0) const;
还有其他可以指定字符数组长度、直接传入单个字符等的重载形式,不过核心功能一致。
(3)案例
题目描述:在一个给定的字符串中查找特定字符集合("abcdv")里的任意字符,将找到的这些字符替换为星号(*),最后输出替换后的字符串。
#include<string>
#include<iostream>
using namespace std;
int main()
{
string str("Please, replace the vowels in this sentence by asterisks.");
cout << str << endl;
//str.find_first_of("abcdv")的功能是在字符串str中出现字符串"abcdv"中的任意一个字符
//就会返回该字符在字符串str中的pos位置,
//若字符串str中没有字符串"abcdv"中的任意一个字符,则查找失败就会返回npos。
size_t found = str.find_first_of("abcdv");
while (found != string::npos)
{
str[found] = '*';
found = str.find_first_of("abcdv", found + 1);
}
cout << str << endl;
return 0;
}

7.2.find_last_of
(1)函数功能:std::string 类的 find_last_of 成员函数用于在字符串中从指定位置开始逆向查找,找出与给定字符集合中任意一个字符匹配的最后一个字符的位置。
(2)函数原型
find_last_of 函数有多个重载版本,常见的如下:
//从 pos 位置开始逆向查找 str 中任意字符最后一次出现的位置
size_t find_last_of(const std::string& str, size_t pos = npos) const noexcept;
//从 pos 位置开始逆向查找以空字符结尾的字符串 s 中任意字符最后一次出现的位置
size_t find_last_of(const char* s, size_t pos = npos) const;
//从 pos 位置开始逆向查找 s 指向的字符数组的前 n 个字符中任意字符最后一次出现的位置
size_t find_last_of(const char* s, size_t pos, size_t n) const;
//从 pos 位置开始逆向查找字符 c 最后一次出现的位置
size_t find_last_of(char c, size_t pos = npos) const noexcept;
8.substr
string::substr - C++ Reference (cplusplus.com)

(1)函数功能:substr 函数可以从调用该函数的字符串对象的pos位置开始提取出len长度的字符序列,形成一个新的 std::string 对象进行返回。
总的来说,substr 成员函数用于从一个字符串中提取子字符串。
(2)函数原型:
//从位置 pos 开始提取长度为 len 的子字符串
std::string substr(size_t pos = 0, size_t len = npos) const;
(3)案例1:
题目描述:从一个文件名(包含可能的多级扩展名)中提取出最后一个扩展名。即把文件名的后缀找出来(注意:后缀是包括 ' .' 字符的)。
思路分析:在文件命名里,扩展名通常是文件名中最后一个点号(.)之后的部分,例如对于文件名 "string.cpp.tar.zip",其扩展名是 .zip。所以我们可以使用成员函数rfind从后往前找出 ' .' 字符的位置pos,并用成员函数substr从源字符串对象的pos位置开始提取len长度的子字符串来解题。
代码实现:

(4)案例2:
题目描述:分别找出一个网址的协议、域名、资源。
思路分析:代码的核心思路是利用 string 类提供的 find 和 substr 函数对给定的 URL 字符串进行解析。通过查找特定的分隔符(如 "://" 和 '/')来确定 URL 中不同部分的边界,然后使用 substr 函数截取相应的子字符串,从而提取出协议、域名和资源路径。
代码实现:
#include <iostream>
using namespace std;
//题目把网址的3个部分分别取出来。协议http(以"://"结束)、域名www.cplusplus.com(以'/'结束)、
int main()
{
string url(https://blog.csdn.net/"http://www.cplusplus.com/reference/string/string/find/");//网址构造 = 协议+域名+资源(UI)
//打印整个网址
cout << url << endl;
//1.使用string substr (size_t pos = 0, size_t len = npos) const取协议
cout << url.substr(0, 4) << endl;//取协议
//2.取域名的过程
//2.1.从头开始使用find找协议名的结束标志"://"的起始地址start,而start + 3就是域名的起始地址
//找域名开头
size_t start = url.find("://");
if (start == string::npos)//若star = npos则说明没有找到"://"。
{
cout << "invalid url" << endl;
}
start += 3;
//2.2.从域名起始位置start开始使用find找域名的结束标志'\'的位置
//找域名结尾。
size_t finish = url.find('/', start);//从第3个位置开始找域名的结束标志'\'的位置。
//2.3.打印域名
string address = url.substr(start, finish - start);
cout << address << endl;
//3.取资源的过程
//3.1.打印域名
string resource = url.substr(finish + 1);
cout << resource << endl;
return 0;
}

9.swap
string::swap - C++ Reference (cplusplus.com)
swap - C++ Reference (cplusplus.com)


9.1.string::swap成员函数 与 std::swap函数模板的对比
(1)string::swap 成员函数与 std::swap 函数模板的功能差异
-
string::swap 成员函数:它是
string类自带的成员函数,其功能专门针对string类型的对象进行交换操作,而且只能应用于string类型。 -
std::swap 函数模板:它是
C++标准库提供的一个通用的函数模板,具有更广泛的适用性,它可以对任意类型的对象执行交换操作,只要该类型满足相应的拷贝构造、赋值等操作要求即可,并不局限于string类型对象。
(2)string::swap 成员函数与 std::swap 函数模板的交换效率对比
- 结论:
string类的swap成员函数效率更高。 - string::swap 成员函数的模拟实现
class string { public: //自定义的swap函数,用于交换两个string对象的内部状态。 void swap(string& s) { //注意:下面调用的是std库提供的swap函数,需要明确指定std命名空间, //避免与自定义的string类成员函数swap产生命名冲突,否则会导致编译报错。 //调用std库中的swap函数交换两个string对象的字符数组指针,使得两个对象指向对方原 //来所指向的字符数组,这样就完成了字符串底层数据存储部分的交换。 std::swap(_str, s._str);//直接改变两个对象成员指针来完成交换 //调用std库中的swap函数交换两个string对象的容量值,使得每个对象能正确知晓自己所 //管理的字符串容量情况。 std::swap(_capacity, s._capacity); //调用std库中的swap函数交换两个string对象的字符串实际长度值,以便后续操作能获取 //正确的长度信息。 std::swap(_size, s._size); } private: char* _str; size_t _capacity; size_t _size; };
- 原因:
string类的底层是通过指针指向动态数组实现的。在执行string::swap成员函数进行交换时,只需改变两个string类对象内部指针的指向,就能达成字符串数据的交换,整个操作较为简单直接,所产生的开销较小。如下图所示:
- 反观
std::swap函数模板,其交换过程相对复杂。大致来说,在交换两个对象时,首先会调用一次深拷贝的拷贝构造函数创建临时对象c,随后分别调用两次深拷贝的赋值运算符重载函数operator=来完成交换。深拷贝操作涉及开辟新内存空间,并把原对象的数据逐个拷贝到新空间里,包含较多的内存分配与数据拷贝操作,相较于string类的swap成员函数,会产生更大的开销,进而导致其效率更低。
(3) C++ 标准库已经提供一个通用的函数模板swap,但为什么string 类还要提供 swap 成员函数的原因。
虽然 C++ 标准库已有通用的 std::swap 函数模板,可处理包含 string 类型在内的各类对象交换,但 string 类依旧提供了专属的 swap 成员函数。根本原因在于 string 类的 swap 成员函数具备更高的效率。这并非早期设计的疏漏,而是从优化 string 类操作效率的角度出发,依据 string 类型自身特点设计了专门的交换方式,如此一来,在涉及 string 对象交换的场景中,能够有效减少不必要的性能损耗,提升代码整体的执行效率。
(4)案例:
以下代码示例展示了 string 类的 swap 成员函数和 C++ 标准库提供的 std::swap 函数模板都可以实现对 string 类对象的交换操作,但是string 类的 swap 成员函数效率更高。
#include<string>
#include<iostream>
using namespace std;
int main()
{
string s1("hello world i love you");
string s2("xxxxx");
//1.调用string类的void swap (string& str)成员函数
s1.swap(s2);
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
//2.调用函数模板swap,如下所示:
/*template <class T>
void swap(T & a, T & b)
{
T c(a);
a = b;
b = c;
}*/
swap(s1, s2);//函数模板swap的隐式实例化
cout << "s1:" << s1 << endl;
cout << "s2:" << s2 << endl;
return 0;
}
10.c_str
string::c_str - C++ Reference (cplusplus.com)
10.1.c_str的介绍
(1)c_str 成员函数的功能
const char* c_str() const;
该函数的主要功能是将 string 对象的内容以 C 风格的字符串形式返回,即返回一个指向以 '\0' 结尾的字符数组的指针。这个字符数组包含了 string 对象中的字符序列。
(2)为什么提出 c_str 函数
兼容C语言:许多 C 语言的库函数(如 printf、strcpy、strlen 等)接受 const char* 类型的参数,而不是 C++ 的 string 类型。使用 c_str 可以将 string 对象转换为 C 风格的字符串,以便调用这些 C 语言库函数。
10.2.c_str成员函数使用案例
(1)案例1:以下两种打印方式的打印结果是没有什么区别的
#include<string>
#include<iostream>
using namespace std;
int main()
{
string s1("hello world");
//1.以下两种打印方式的打印结果是没有什么区别的。
//1.1.调用自定义类型的流插入运算法重载函数operator cout来打印
cout << s1 << endl;
//1.2.调用C++的std库中提供内置类型的ostream cout
cout << s1.c_str() << endl;//s1.c_str()返回的是string类对象s1的成员变量指针_str,而指针_str是内置类型
//注意:const char* c_str() const的返回值是char*类型,所以s1.c_str()的返回值指针指向一个字符串。
//1.3.当c_str()的返回值类型被强制类型转换成void*,则(void*)c_str()的打印结果就是c_str()返回值中存放的地址。
cout << (void*)s1.c_str() << endl;//打印结果:打印的是s1.c_str()返回值指针中存放的地址。
return 0;
}
//注意:cout对于非char*类型的指针,cont会按指针的值打印;cout对于char*类型的指针,cout会按字符串
//打印。由于(void*)s1.c_str()返回的指针类型被强制类型转换为void*类型使得cout对于该指针是按指针的
//值打印的,而s1.c_str()返回的指针类型是char*,而对于char*类型的指针是按字符串打印的。
(2)案例2:以下两种打印方式的打印结果存在差异
#include<string>
#include<iostream>
using namespace std;
int main()
{
string s1("hello world");
//2.以下两种打印方式的打印结果存在差异
cout << "s1修改前:" << endl;
cout << s1 << endl;
cout << s1.c_str() << endl;
s1 += '\0';
s1 += '\0';
s1 += "xxxxx";
cout << "s1修改后:" << endl;
//2.1.方式1:
cout << s1 << endl;//string类的流插入运算法重载函数operator cout是按size去打印字符串,是不管字符串终止符'\0'。
//2.2.方式2:
cout << s1.c_str() << endl;//当我们使用std库提供的内置类型的流插入运算法重载函数ostream cout时,对于字符指针的打印,当遇到
//字符串终止符'\0'时会停止打印。总的来说,std库提供的ostream cout是管字符串终止符'\0'的。
return 0;
}
总结:按流插入运算符重载函数打印cout << s1 << endl则是按size的值去打印的(即size = 18,就打印18个字符);而按c_str()函数打印cout << s1.c_str() << endl则由于c_str()返回的是char*类型的指针,使得cout << s1.c_str() << endl是不按size的值去打印,而是cout会识别char*类型的指针去按字符串打印而按字符串打印则遇到字符串终止符'\0'就会停止打印。总的来说,cout << s1 << endl不管s1中存放多少字符串终止符'\0',都是打印size个字符;而cout << s1.c_str() << endl则是遇到字符串终止符'\0'就会停止打印。
(3)案例3:string类要提供c_str()成员函数是为了兼容C语言的函数接口。例如:在C++代码中使用C语言的fopen函数接口来打开string类类型的文件名,由于fopen函数接口无法识别string类类型使得C++提供c_str()来返回string类的char*类型指针成员变量来传参给fopen函数接口。
string类要提供c_str()成员函数的原因:C语言对于字符串的打印是遇到字符串终止符'\0'就会停止,但是流插入运算符重载函数cout << s1对于字符串类对象s1的打印不是遇到终止符'\0'就会停止打印的而是按size的值打印,但是cout << s1.c_str()是遇到字符串终止符'\0'就会停止打印,所以我们才提供c_str()成员函数来打印字符串对象s1。总的来说,C++提供c_str()成员函数的原因是和C语言的函数接口进行兼容;例如,C语言的函数FILE * fopen ( const char * filename, const char * mode )的参数const char * filename是个char*类型的指针,而C++是用string类存放文件名filename,而在C++代码中为了可以调用C语言的接口函数fopen来打印文件,则C++提出c_str()成员函数返回string类的char*类型的指针成员变量来兼容C语言函数接口fopen的char*类型的形参const char * filename。
#include<string>
#include<iostream>
using namespace std;
int main()
{
//3.分析string类要提供c_str()成员函数的原因:和C语言的某些场景进行兼容。场景:和C语言的函数接口进行兼容。
//案例:若文件名是string类,则c_str()成员函数的返回值可以和C语言提供的fopen函数的第一个参数类型进行兼容。
//3.1.案例:以读的形式打开文件
//创建一个string类对象filename,filename对象中存放的数组字符串"test.cpp"就是文件名
string filename("test1.cpp");//文件名用string类对象存储
//以C语言的方式,以读的形式打开test.cpp源文件
FILE* fout = fopen(filename.c_str(), "r");//filename.c_str()返回的是指向文件名的指针_str
if (fout == nullptr)
perror("fopen fail");
char ch = fgetc(fout);//读取文件第一个字符
while (ch != EOF)
{
cout << ch;//打开读取到的字符
ch = fgetc(fout);//读取文件中的下一个字符
}
fclose(fout);//关闭文件
return 0;
}
string类对象的访问及遍历操作
注意:在string类中,我们一般很少使用迭代器访问数据的,而是经常使用operator[]来随意访问string类对象中的数据。
1.迭代器的使用
1.1.iterator(正向) 、reverse_iterator(反向)类型的迭代器使用
string::begin - C++ Reference (cplusplus.com)
string::end - C++ Reference (cplusplus.com)
string::rbegin - C++ Reference (cplusplus.com)
string::rend - C++ Reference (cplusplus.com)
访问字符串类对象所有元素的3种方式:
1.1.1.访问方式1:使用下标[]运算符重载函数char& operator[] (size_t pos) 或者 const char& operator[] (size_t pos) const 来 获取字符串的字符
注意:char& operator[] (size_t pos)返回的是pos位置字符的引用,则我们可以通过返回值改变pos位置字符的值。 const char& operator[] (size_t pos) const返回的是pos位置字符的const引用,则我们就不可以通过返回值改变pos位置字符的值。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1("hello world");
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i] << " ";
}
cout << endl;
return 0;
}
1.1.2.访问方式2:使用迭代器进行访问
(1)正向迭代器遍历访问:使用begin()、end(),迭起器正着遍历。
int main()
{
string s1("hello world");
//使用迭代器访问string类对象s1中的字符串所有元素的方式:
//注意:由于string::iterator指的是iterator是在string类中定义的类型,所以我们在使用
//迭代器时,一定要指定迭代器类型自己所在的类域。
string::iterator it = s1.begin();
//注意:s1.end()指向字符串对象最后一个有效数据的下一个位置,
//所以是左边右开[begin,end)
while (it != s1.end())
{
*it += 1;
cout << *it << " ";
++it;
}
cout << endl;
return 0;
}
①如何理解使用迭代器可以做到访问存放在对象中的数据。
注意::实际上,迭代器有时候可以是指针,但有时候也不是指针。在Linux下,迭代器的底层是个指针;但是在VS下,迭代器的底层不是指针。但是下面就假设迭代器是指针来介绍使用迭代器是如何做到访问存放在string类对象中的字符。所以下面把迭代器想象成指针来访问string类对象,如下图所示:
解析:s1对象中有一个iterator类型的指针(即迭代器变量it)指向动态数组的空间,而s1.begin()成员函数返回的是指向动态数组起始位置的指针,而s1.end()返回的是动态数组中最后一个有效数据的下一个位置的指针,而string::iterator it = s1.begin()中的变量it就是迭代器(指针)变量。
总结:迭代器给的区间是左闭右开的区间。
(2)反向迭代器遍历访问:使用rbegin()、rend(),让迭起器倒着遍历。
注意:
- 倒着遍历的迭起器叫做反向迭代器,用reverse_iterator自定义类型表示反向迭代器。
- rbegin()指向字符串对象最后一个有效数据,而rend()指向字符串对象首元素的前一个位置。
- 反向迭代器++是往左走,- - 往右走。正向迭代器++往右走,- - 往左走。所以下面代码反向迭代器rit必须是++才算是反向遍历(即rit++),而不是 - - (即rit--)。总的来说,反向迭代器是++才是往正向走即往左走,而 - - 是往反向走即往右走;正向迭代器是++ 才是往正向走即往右走,而 - - 是往反向走即往左走。
#include<string>
#include<iostream>
using namespace std;
int main()
{
string s1("hello world");
//使用反向迭代器,倒着变量字符串
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
return 0;
}
(3)正反向迭代器遍历string类对象的案例
#include<string>
#include<iostream>
using namespace std;
int main()
{
string s1("hello world");
//使用迭代器,正着遍历字符串
string::iterator it = s1.begin();//begin()函数的返回值指针指向字符串第一个字符
while (it != s1.end())//end()函数的返回值指针指向字符串最后一个有效字符的后一个位置
{
cout << *it << " ";
//注:在VS中,暂时把迭代器理解成指针(注:实际不是指针)
++it;//对于迭起iterator来说,++it是让迭代器(指针)从起点begin()往终点end()
//这个正方向移动进而正着遍历字符串。
}
cout << endl;
//使用反向迭代器,倒着遍历字符串
string::reverse_iterator rit = s1.rbegin();//rebgin()函数的返回值指针指向字
//符串最后一个有效字符。
while (rit != s1.rend())//rend()函数的返回值指针指向字符串第一个字符的前一个位置。
{
cout << *rit << " ";
++rit;//对于迭代器reverse_iterator来说,++rit是让迭代器(指针)从起点rbegin()
//往终点rend()这个正方向移动进而倒着遍历字符串。注意:这里一定不要把
//++rit写成--rit,不然一定会发生报错,而报错的原因是越界进行非法访问。
}
cout << endl;
return 0;
}
1.1.3.方式3:范围for(注:范围for的底层原理是迭代器)
(1)范围for的使用格式
//范围for循环的语法格式:
#include<iostream>
using namespace std;
//类型1:只打印访问对象存放的数据,不修改数据
for (auto 临时变量名 : 访问对象)//注:访问对象类型:内置类型 或者 自定义类型
{
cout << 临时变量名 << endl;
}
cout << endl;//换行
//类型2:打印访问对象存放啊的数据,也修改数据
for (auto& 临时变量名 : 访问对象)//注:访问对象类型:内置类型 或者 自定义类型
{
cout << 临时变量名 << endl;
//这里可以通过修改临时变量(引用)来改变访问对象中存放数据的值
}
cout << endl;//换行
(2)案例
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1("hello world");
//利用范围for访问string类对象所有数据
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
return 0;
}
(3)从汇编角度证明范围for循环的底层原理是迭代器
图形解析:从范围for的汇编代码可知范围for循环的过程中调用begin()、end()成员函数从而证明范围for循环的底层原理是迭代器。
结论:由于范围for代码的语法在编译期间,编译器直接把范围for循环的语法替换成迭代器代码的语法,所以我们才说范围for的底层实现是迭代器。
1.2.const_iterator(正向)、const_reverse_iterator(反向)类型的迭代器使用
string::cbegin - C++ Reference (cplusplus.com)
string::cend - C++ Reference (cplusplus.com)
string::crbegin - C++ Reference (cplusplus.com)
1.2.1.begin()和end()、rbegin()和rend()可以分别返回const正反向迭代器
(1)案例1:const正向迭代器——string类对象使用传引用传参且形参被const时,形参const string类对象的遍历需要使用const_iterator类型的迭代器。
注意:
- const string类对象的迭代器类型一定是const_iterator类型,即const迭代器;而非const string类对象的迭代器类型一定是iterator,即非const迭代器。
- const string类对象只能使用const_iterator类型的迭代器进行遍历;非const string类对象可以使用const_iterator 或者 iterator类型的迭代器进行遍历。
- 当string类对象使用传引用传参时,若在函数体内部不会修改string类对象,则一般会使用const修饰参数string类对象,则此时若需遍历const string类对象时只能使用const_iterator类型的迭代器,而不能使用iterator类型的迭代器。
- 把const_iterator类型的const迭代器赋值给iterator类型的非const迭代器时一定会发生报错。
①错误案例解析
//错误案例
#include<string>
#include<iostream>
using namespace std;
//传引用传参
void Func(const string& s)
{
//注意:由于begin函数有2种类型而且返回值类型都不同,所以在使用string类对象访问begin
//成员函数时该begin函数的返回值取决于该对象是否被const修饰,若对象被const则begin函数
//一定返回const_iterator类型的迭代器;若对象是非const修饰则begin函数一定返回iterator
//类型的迭代器。
string::iterator it = s.begin();//1.报错原因:由于string类对象s是被const修饰,则s.begin()
//的返回值类型是const_iterator类型的const迭代器,但是表达式string::iterator it = s.begin()
//中的it变量类型是iterator类型的非const迭代器,若是把const_iterator类型的s.begin()
//赋值给iterator类型的it则程序一定会发生报错,因为权限放大了。
//总的来说,该程序发生报错的原因是:赋值过程中类型转换发生错误。
//2.为什么存在const_iterator类型的const迭代器?
//解答:若迭代器it的类型是const_iterator(即const迭代器),当我们把迭代器想象成指针时,
//则指针it对其所指向的空间只有只读权限而没有写权限。若我们想要遍历const string类对象时,
//由于const string类对象是不能修改的,即我们只有对const string类对象进行读而不能写,
//所以我们才需要const_iterator类型的const迭代器来遍历const string类对象。
while (it != s.end())
{
cout << *it << " ";
++it;//注:由于string类对数s是被const修饰的,但是在这里*it代码是没有错误,因为*it只是
//读取指针it指向空间中的数据并没有修改*it的值,所以*it代码功能是只读。但表达式
//*it += 1是对指针it指向空间中的数据进行修改,所以*it += 1代码功能是写,进而一定
//会发生报错。
}
cout << endl;
}
int main()
{
string s1("hello world");
Func(s1);
return 0;
}
②解决方式:把string::iterator it = s.begin() 修改成 string::const_iterator it = s.begin() 或者
auto it = s.begin() 就不会发生报错。
#include<string>
#include<iostream>
using namespace std;
//传引用传参
void Func(const string& s)
{
string::const_iterator it = s.begin();
//或者写成:auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
int main()
{
string s1("hello world");
Func(s1);
return 0;
}
(2)案例2:const反向迭代器
注意:const反向迭代器只能读不能写。
#include<string>
#include<iostream>
using namespace std;
void Func(const string& s)
{
string::const_reverse_iterator rit = s.rbegin();
//注:这里的赋值必须用const修饰反向迭代器来接收,否则会发生报错。
while (rit != s.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
}
int main()
{
string s1("hello world");
Func(s1);
return 0;
}
(3)案例3:使用auto可以自动推导正反向const迭代器的类型
注意:auto可以自动推导const正反向迭代器 或者 非const正反向迭代器的类型。
#include<string>
#include<iostream>
using namespace std;
void Func(const string& s)
{
auto it = s.begin();//注意:auto推导出变量it的类型是const_iterator
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
auto rit = s.rbegin();//注意:auto推导出变量rit的类型是const_reverse_iterator
while (rit != s.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
}
int main()
{
string s1("hello world");
Func(s1);
return 0;
}
(4)总结
①遍历和修改 string 类数据的选择方式:对于string类而言,在遍历和修改 string 类存放的数据时,相比迭代器使用上太过复杂,使用下标[ ]运算符重载函数 operator[] 或者范围 for 循环通常更加方便、合理。
②string类要学习迭代器的原因:
- 适用于多种数据结构:迭代器具有通用性,适用于多种数据结构的遍历和修改。像
string类、顺序表类等底层基于数组(连续物理空间)的数据结构,可以重载operator[]来实现便捷的遍历和修改操作。然而,对于链表这种底层空间不是连续的(非数组结构)数据结构,很难重载operator[]来进行有效的遍历和修改,同理,树结构也无法通过重载operator[]来实现遍历。所以,迭代器能应用于各种不同类型的数据结构(包括链表、树等以及其他容器)进行遍历和修改,而operator[]只适用于底层是数组(连续物理空间)实现的数据结构(例如string类、顺序表类、栈类等)。 - 支持泛型算法:C++ 的标准库中包含了大量的泛型算法,如
sort、find、for_each等,这些算法都是基于迭代器来操作数据的。通过使用迭代器,string类可以方便地与这些泛型算法结合使用,从而实现各种复杂的数据处理操作,而无需为string类单独编写大量的算法函数。例如,可以使用sort算法对string中的字符进行排序,只需要传递string的迭代器范围即可。总的来说,当使用各种算法去访问数据结构(类类型)中的数据时,通常是借助迭代器来进行访问的。 - 迭代器与范围 for 的关系:范围
for循环是 C++ 中一种方便的遍历容器的方式,其底层原理就是基于迭代器实现的。对于string类,使用范围for循环可以简洁地遍历字符串中的每个字符,而实际上是编译器将范围for循环转换为使用迭代器的形式来实现遍历。
③const_iterator 与 iterator 类型迭代器的权限区别:
const_iterator类型的迭代器(可理解为一种指针)只有只读权限,只能用于遍历和读取容器的数据,而不能对容器中的数据进行写(修改)操作。普通的iterator类型迭代器则具备遍历和读写容器数据的权限。二者的主要区别就在于是否能对容器中的数据进行写(修改)操作。同理,const 或者 非const的反向迭代器的权限区别也是类似的。- 值得注意的是,权限放大和权限缩小的问题通常出现在指针和引用上,由于迭代器也可以类比理解成一种特殊的指针,所以迭代器同样存在权限放大和权限缩小的问题。
④迭代器一共有 4 种类型,分别是 iterator 、const_iterator、reverse_iterator、const_reverse_iterator,各自有着不同的使用场景和特点,用于满足在不同需求下对容器进行遍历、读写以及反向遍历等操作。
1.2.2.cbegin()和cend()、crbegin()和crend()只分别返回const正反向迭代器
注意:
- cbegin()函数、cend()函数只能返回const_iterator类型的正向迭代器;而begin()、end()返回的是iterator 或者 const_iterator类型的正向迭代器。
- crbegin()函数、crend()函数只能返回const_reverse_iterator类型的反向迭代器;而rbegin()、rend()返回的是reverse_iterator 或者 const_reverse_iterator类型的反向迭代器。
- cbegin()和cend()、crbegin()和crend()等函数提出的原因:引入
cbegin()和cend()、crbegin()和crend()这些函数的主要目的在于,当使用迭代器相关的成员函数来遍历容器(这里以string类对象为例)时,能够清晰地区分对待const对象和非const对象的情况。cbegin()和cend()、crbegin()和crend()明确是专门设计用来遍历const修饰的string类对象的,它们返回的迭代器类型确保了在遍历过程中不会意外修改容器中的元素,保证了对const对象的只读性遵循const的语义约束。而begin()和end()、rbegin()和rend()主要用于非const的string类对象遍历,不过它们也具备对const对象进行遍历的能力(此时返回的是只读类型的迭代器)。 -
使用建议:虽然 begin() 和 end()、rbegin() 和 rend() 能够用于遍历 const 修饰的 string 类对象,但为了保证代码的清晰性以及严格遵循 const 语义,更好地体现对 const 对象只读操作的明确性,在遍历 const 字符串对象时,建议使用 cbegin() 和 cend()、crbegin() 和 crend() 等函数;而在遍历非 const 的 string 类对象时,则使用 begin() 和 end()、rbegin() 和 rend() 函数。但是实际当中,我们一般使用begin() 和 end()、rbegin() 和 rend()来同时遍历 const 或者 非const的字符串对象。
#include<string>
#include<iostream>
using namespace std;
void Func(const string& s)
{
auto it = s.cbegin();//注意:auto推导出变量it的类型是const_iterator
while (it != s.cend())
{
cout << *it << " ";
++it;
}
cout << endl;
auto rit = s.crbegin();//注意:auto推导出变量rit的类型是const_reverse_iterator
while (rit != s.crend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
}
int main()
{
string s1("hello world");
Func(s1);
return 0;
}
2.at成员函数——char& at (size_t pos)、const char& at (size_t pos) const的使用
string::at - C++ Reference (cplusplus.com)
string::operator[] - C++ Reference (cplusplus.com)


注意:char& at (size_t pos)、const char& at (size_t pos) const都是返回pos位置的字符。即at成员函数和operator[]成员函数都是返回pos位置的字符,即它们的功能都一样。
2.1.at 和 operator[]的区别
注意,at 和 operator[]的共同点:
- 功能:两者的主要功能都是提供对容器或类中元素的访问机制,允许通过索引来获取或修改特定位置的元素。
- 返回值类型:通常情况下,它们返回的都是所访问元素的引用(对于不能修改元素的常量对象,会返回常量引用),这使得它们都可以用于读取元素的值,并且在可修改的情况下,也都能用于修改元素的值。
(1)operator[]对于越界访问的处理方式
- 直接报错并终止程序:当使用
operator[]进行越界访问时,它通常不会进行专门的错误处理,而是直接导致程序报错并终止。这是因为operator[]主要设计用于快速访问元素,并不进行严格的边界检查。在函数体内部,往往是使用断言assert进行报错,一旦触发断言,程序就会终止。例如,在访问数组或类似数组的数据结构时,如果使用operator[]访问超出范围的元素,就会出现这种情况。
(2)at对于越界访问的处理方式
- 抛出异常:
at成员函数在遇到越界访问时,会抛出一个异常,而不是直接报错终止程序。这使得程序员有机会使用try-catch语句来捕获并处理这个异常。 - 可捕获异常,程序不直接终止:一旦异常被捕获,程序不会立即终止,而是可以根据具体的异常处理逻辑进行相应的操作,比如打印错误信息后再结束程序,或者进行一些恢复操作等,从而使程序具有更好的健壮性和容错性。
①对于抛异常的处理方式
try
{
//这里写可能会抛出异常的代码。
}
catch(const exception& e)
{
cout << e.what() << endl;
}
在C++中,上述代码展示了异常处理的基本结构。具体来说,这段代码的作用如下:
-
try块:
try关键字后面的大括号{ }内的部分称为“try块”。在这个块中,你会放置可能会抛出异常的代码。如果在执行这些代码时发生了异常,它会立即被抛出,并且执行流程会跳转到相应的catch块。 -
catch块:
catch关键字用于捕获并处理异常。括号内定义了要捕获的异常类型,这里使用的是const exception& e,意味着它可以捕获任何继承自std::exception类的异常对象。std::exception是C++标准库中定义的一个异常类,它是一个基类,用于所有可以通过what()方法返回错误信息的异常。const关键字表示捕获的异常对象是一个常量引用,这意味着在catch块中不能修改异常对象。&表示按引用捕获异常,这样可以避免复制异常对象,特别是当异常对象较大时,这可以提高效率。
-
异常处理:在
catch块内部,使用cout(标准输出流对象)和e.what()来输出异常信息。e.what()是std::exception类的一个成员函数,它返回一个const char*指向的字符串,该字符串描述了异常的具体信息。
综上所述,这段代码的作用是:
- 执行
try块中的代码。 - 如果在执行过程中
try块中的代码抛出了任何继承自std::exception的异常,控制流会跳转到相应的catch块。 - 在
catch块中,通过调用捕获到的异常对象的what()方法来输出异常信息,然后程序继续执行catch块之后的代码。
如果 try 块中没有异常被抛出,catch 块将不会执行。如果抛出了不是 std::exception 或其派生类的异常,这个特定的 catch 块也不会捕获它,除非有其他匹配的 catch 块存在。
②案例
-
案例1:没有异常处理,at成员函数发生抛异常后会直接终止程序。
-
案例2:有异常处理,at成员函数发生抛异常后不会直接终止程序。
2.2.在实际当中我们很少使用at成员函数,而是经常使用operator[ ]的原因
- 习惯与便捷性
- 语法简洁直观:
operator[]的语法与数组访问相似,使用起来非常自然。例如,要访问string对象s中的第i个字符,使用s[i]的形式简洁明了,符合大多数开发者的编程习惯。而使用at函数则需要写成s.at(i),相对来说略显繁琐。 - 支持链式操作:
operator[]返回的是字符的引用,这使得它可以很方便地用于链式操作。例如,可以直接对s[i]进行赋值操作,如s[i] = 'a',还可以将其作为参数传递给其他函数等。而at函数虽然也返回字符的引用,但由于其函数调用的形式,在一些复杂的链式操作中可能会显得不够简洁。
- 语法简洁直观:
- 性能考虑
- 避免边界检查开销:在访问
string中的字符时,operator[]通常不进行边界检查(在 Release 版本中),因此访问速度更快。而at函数在每次调用时都会进行严格的边界检查,以确保索引在合法范围内,这会带来一定的性能开销。在对性能要求较高且能确保索引合法的场景下,operator[]的性能优势就更加明显。 - 优化编译器优化:由于
operator[]的简单性,编译器更容易对其进行优化。例如,编译器可能会将s[i]的访问直接优化为指针偏移操作,直接通过字符串的首地址和偏移量来访问字符,而对于at函数的调用,编译器需要处理函数调用的开销以及边界检查的代码,优化空间相对较小。
- 避免边界检查开销:在访问
- 功能需求匹配
- 已知索引合法的场景:在很多实际应用中,开发者在使用索引访问
string中的字符时,已经通过其他方式确保了索引的合法性。例如,在一个根据string长度进行循环遍历的场景中,循环条件已经保证了索引不会越界,此时使用operator[]可以更高效地访问字符,而无需at函数的边界检查。 - 异常处理的复杂性:虽然
at函数在越界时会抛出异常,这在某些情况下可以帮助捕获错误,但在一些简单的程序中,处理异常可能会增加代码的复杂性。如果开发者能够在代码逻辑中确保索引的合法性,那么使用operator[]可以避免不必要的异常处理代码,使代码更加简洁。
- 已知索引合法的场景:在很多实际应用中,开发者在使用索引访问
3.back、front成员函数
注意:front成员函数返回字符串第一个字符的引用, back成员函数返回字符串最后一个字符的引用(注:最后一个字符指的是最后一个有效字符)。但是在实际当中我们一般很少使用back、front成员函数,而是使用operator[]重载函数来实现back、front成员函数获取首尾字符的功能,即
front() <=> operator[0] 、back() <=> operator[size - 1] 。
string类非成员函数
1.relational operators
relational operators (string) - C++ Reference (cplusplus.com)

operator==
STL 的string类怎么啦?_string类是stl类-CSDN博客
#include<string>
#include<iostream>
using namespace std;
int main()
{
string s1("hello world");
string s2("hello world");
s1 == s2;
//1.可以只实现bool operator== (const string& lhs, const string& rhs)而不实现
//bool operator== (const char* lhs, const string & rhs) 和
//bool operator== (const string & lhs, const char* rhs)
//都可以支持以下两种判断的原因:
//解答:单参构造函数支持隐式类型转换
s1 == "hello world";//字符串"hello world"可以通过单参构造函数隐式类型转换为string类的对象
"hello world" == s1;//字符串"hello world"可以通过单参构造函数隐式类型转换为string类的对象
return 0;
}

2.getline
getline (string) - C++ Reference (cplusplus.com)

(1)函数功能:getline 并非 std::string 类的成员函数,而是标准库 <string> 中提供的一个全局函数。它的主要功能是从输入流中读取一行文本,并将其存储到 std::string 对象里。
注意:getline非成员函数可以读取任意字符包括空格字符。
(2)函数模型:
//从输入流 is 中读取一行文本到字符串 str 中,以换行符 '\n' 作为行结束标志
istream& getline (istream& is, string& str);
//从输入流 is 中读取一行文本到字符串 str 中,以指定的分隔符 delim 作为行结束标志
istream& getline (istream& is, string& str, char delim);
(3)案例
#include <iostream>
#include<string>
using namespace std;
int main()
{
string str;
//输入字符串的两种写法和限制:
//1.写法一:使用流提取运算符重载函数cin来输入,但是只能输入不带空格字符的字符串。
//注:cin在读取字符串时遇到空格字符就结束读取。
cin >> str;
cout << str << endl;
//2.写法二:使用C++的std库提供的getline来输入,而输入的字符串可以带空格字符。
//注:getline在读取字符串时只有遇到换行符才结束读取。
getline(cin, str);
cout << str << endl;
return 0;
}

结论:在C++中,若读取带有空格字符的字符串时只能使用getline函数读取,而不能使用cin读取字符串;若读取不带有空格字符的字符串时可以使用getline函数或者cin来读取字符串。
(4)注意事项
在 C++ 中,std::cin 和 std::getline 都可用于从标准输入读取数据,但它们在功能、使用方式等方面存在明显区别,详细介绍如下:
①基本功能
std::cin:它是标准输入流对象,主要用于从标准输入设备(通常是键盘)读取各种类型的数据,包括整数、浮点数、字符和字符串等。当用于读取字符串时,它默认以空白字符(空格、制表符、换行符等)作为输入的分隔符,遇到空白字符就会停止读取。std::getline:它是一个全局函数,用于从输入流(如std::cin)中读取一行文本,并将其存储到std::string对象中。它可以指定分隔符,默认以换行符'\n'作为行的结束标志。
②读取字符串的方式
std::cin:读取字符串时,遇到空白字符就会停止。例如:如果输入Hello World,std::cin只会读取到Hello,因为遇到空格就停止了。std::getline:默认读取一整行,直到遇到换行符。例如:如果输入Hello World,std::getline会读取整行内容Hello World。
③分隔符处理
std::cin:默认以空白字符作为分隔符,无法直接指定其他分隔符。std::getline:可以通过第三个参数指定分隔符。例如:getline指定逗号,作为分隔符,std::getline会读取直到遇到逗号为止的文本。#include <iostream> #include <string> int main() { std::string str; std::cout << "请输入一些文本,以逗号分隔: "; std::getline(std::cin, str, ','); std::cout << "分隔符之前的文本是: " << str << std::endl; return 0; }
③缓冲区处理
std::cin:读取数据后,输入缓冲区中剩余的内容(如换行符)会保留。如果后续紧接着使用std::getline读取,可能会导致问题。例如:当输入一个整数并按下回车键后,std::cin读取整数,输入缓冲区中会留下换行符。接着调用std::getline时,它会直接读取到这个换行符,认为输入为空行,导致无法正常读取用户输入的字符串。可以使用std::cin.ignore()来清除缓冲区中的换行符。std::getline:读取一行数据后,会将分隔符从输入缓冲区中移除。
④适用场景
std::cin:适用于读取单个单词、数字等不包含空白字符的数据,或者需要逐个读取多个数据项的场景。std::getline:适用于需要读取包含空格的整行文本的场景,如读取段落、地址等。

3672

被折叠的 条评论
为什么被折叠?



