← 目录 / 第十一章 · STL 标准模板库 / 11.1 string

11.1 string 字符串

C++ 标准库提供的文本处理类型——比 C 风格字符数组更安全、更灵活,是处理字符串的首选工具。

本页目录

C++ 标准库提供了 string 类型,专门用来处理文字信息。你可以把它想象成一条可以自动变长的纸带:纸带上一格放一个字符,你往后面接多少字,它就自己变长多少,不用你操心“够不够放”。使用前需引入 <string> 头文件(如果你用的是万能头文件 <bits/stdc++.h>,就不用再单独写了)。

11.1.1 string 是什么

先分清两个容易混的东西:char 只能放一个字符,用单引号,比如 'A';string 可以放一串字符,用双引号,比如 "Hello"。

⚠️
单引号和双引号不能混用:char c = 'A'; 是对的,char c = "A"; 是错的("A" 是一个字符串,只是恰好只有一个字符)。这是新手最常见的编译错误之一。

在 string 出现之前,C 语言里用字符数组 char s[100] 来存文字,要自己操心数组够不够大、结尾有没有 \0、拼接要调用 strcat……很容易出错。string 把这些麻烦事都包办了:

对比项char 数组string
内存管理手动管理,容易越界、容易出错自动管理,长度自动伸缩
拼接strcat() 函数+ 或 += 运算符
比较strcmp() 函数==、!=、<、> 运算符
获取长度strlen() 函数.size() 或 .length()
越界保护没有保护,容易崩溃.at() 有越界检查
💡
一句话记住:用 string 写字符串,就像用 + 加数字、用 == 比数字一样自然。所以在竞赛和日常练习中,处理文字请优先使用 string。
11.1.2 定义与初始化

下面这个完整的小程序展示了 string 的几种“出生方式”。后面的代码片段为了节省篇幅,都省略了 #include 和 main(),你把它们放进自己的 main() 里就能运行。

C++ · string 的几种初始化方式
1#include <iostream>
2#include <string> // 必须包含这个头文件
3using namespace std;
4
5int main()
6{
7 string s1; // 空字符串,长度为 0
8 string s2 = "Hello"; // 从 C 风格字符串初始化
9 string s3("World"); // 另一种写法,效果和上一行一样
10 string s4(s2); // 拷贝:s4 得到 "Hello"
11 string s5(5, 'a'); // "aaaaa"——5 个字符 'a'
12 string s6 = "Hello"s; // (C++14 起)字面量写法,加 s 后缀表示这是 string
13
14 s1 = "C++"; // 声明之后,随时可以用 = 重新赋值
15 cout << s1 << endl; // 输出:C++
16
17 return 0;
18}
⚠️
注意:
· 没有初始化的 string 默认是空字符串(长度为 0),不会像字符数组那样出现乱码。
· 虽然 string 支持通过下标 [] 访问字符,但不能给还不存在的位置赋值:空字符串 s1[0] = 'a'; 是错误的。想加字符请用 +=(见 11.1.3)。
· "Hello"s 这种带 s 后缀的写法是 C++14 才引入的语法,需要编译器支持 C++14 或更高标准(并且写了 using namespace std;)才能用。如果不确定自己的编译器是否支持,可以暂时忽略这种写法——前面几种初始化方式已经完全够日常使用了。

📏 长度、判空与清空

用 .size() 或 .length() 可以得到字符串里字符的个数,两者功能完全相同,用哪个都行。另外还有两个很常用的小工具:.empty() 判断是不是空字符串,.clear() 把字符串清空。

C++ · size / length / empty / clear / swap
1string s = "Hello";
2cout << s.size() << endl; // 输出:5
3cout << s.length() << endl; // 输出:5,和 size() 完全一样
4
5string t; // 空字符串
6cout << t.empty() << endl; // 输出:1(true),说明 t 是空的
7
8s.clear(); // 清空 s
9cout << s.size() << endl; // 输出:0
10
11string a = "left", b = "right";
12a.swap(b); // 交换两个字符串的全部内容(速度极快)
13cout << a << " " << b << endl; // 输出:right left
🚫
坑 1:size() 返回的是“无符号整数”。s.size() 的类型是 size_t(无符号整数)。如果 s 是空字符串,s.size() - 1 不会得到 -1,而是变成一个超级大的数(约 1.8×1019),用在循环里会直接越界崩溃。
所以在需要做减法或和 int 比较时,先把它转成 int:int n = (int)s.size();,这样 n - 1 就能正常得到 -1 了。
🚫
坑 2:中文字符不是“一个字符”。一个汉字在 string 里占好几个字节:在 UTF-8 编码下占 3 个,在 GBK 编码下占 2 个。所以 string s = "你好"; 的 s.size() 是 6(UTF-8)或 4(GBK),不是 2。
size() 数的是字节,不是“字”。对中文字符串用 s[i] 逐个访问会把一个汉字拆成碎片。竞赛题里的字符串基本都是英文字母和数字,一般不会遇到这个问题;但自己写带中文的程序时要记得这一点。
11.1.3 拼接与比较

🔗 拼接:+ 和 +=

string 最方便的地方,就是可以像加数字一样“加”字符串:+ 把两个字符串连成一个新的字符串;+= 直接在原字符串末尾追加内容。追加单个字符也一样可以用 +=。

C++ · 字符串的拼接
1string s1 = "Hello";
2string s2 = " World";
3string s3 = s1 + s2; // 用 + 拼接,得到新字符串
4cout << s3 << endl; // 输出:Hello World
5
6string s4 = "C++";
7s4 += " Fun"; // 用 += 在 s4 末尾追加
8cout << s4 << endl; // 输出:C++ Fun
9
10string s5 = "ABC";
11s5 += 'D'; // 追加单个字符
12cout << s5 << endl; // 输出:ABCD
13
14// 连续拼接:从左到右一个一个算
15string s6 = "I" + string(" love ") + string("C++");
16cout << s6 << endl; // 输出:I love C++
🚫
两个“双引号字符串”不能直接相加!"Hello" + " World" 会编译报错,因为 "Hello" 和 " World" 都只是“C 风格字符串”,不是 string,不能用 +。
只要加法的两边至少有一边是 string 就可以了:s1 + " World" 没问题;或者像上面那样,用 string("...") 把其中一个“变身”成 string。注意 + 是从左往右算的,所以 "I" + string(" love ") 先算,结果是 string,后面的 + 就都没问题了。

除了 +=,还有两个等价的“追加”工具:s.append(t) 在末尾追加字符串,s.push_back(c) 在末尾追加一个字符;反过来,s.pop_back() 删掉最后一个字符。

C++ · append / push_back / pop_back
1string s = "Hello";
2s.append(" World"); // "Hello World"——在末尾追加字符串
3s.push_back('!'); // "Hello World!"——在末尾追加一个字符
4s.pop_back(); // "Hello World"——删除最后一个字符

⚖️ 比较:== != < >

string 可以直接用比较运算符:== 判断两个字符串是不是完全一样,!= 判断是不是不一样;<、>、<=、>= 则按字典序比较大小——就像查英语词典:先比第一个字母,谁的字母排在前面谁就“更小”;第一个字母一样,再比第二个……

📖
'字符串比较的规则(字典序):
① 从第一个字符开始,逐个字符比较它们的数值(也就是 char 的整数值,通常是 ASCII 码)。
② 遇到第一个不相等的字符时,这个字符较小的那个字符串就更小,后面的字符不用再看。
③ 如果一直比到其中一个字符串结束,前面的字符都相等,那么较短的字符串更小(相当于短的那个是长的那个的“前缀”)。
④ 如果两个字符串长度一样,而且每个字符都相等,那么它们相等(== 为 true)。
C++ · 相等与字典序比较
1cout << boolalpha; // 让 true / false 显示成英文单词,而不是 1 / 0
2
3string s1 = "apple", s2 = "apple", s3 = "banana";
4cout << (s1 == s2) << endl; // 输出:true,内容完全一样
5cout << (s1 != s3) << endl; // 输出:true,内容不一样
6cout << (s1 < s3) << endl; // 输出:true,因为 'a' < 'b'
7cout << (s3 > s1) << endl; // 输出:true
8
9// 一方是另一方的前缀时,更短的更小
10string s4 = "abc", s5 = "abcd";
11cout << (s4 < s5) << endl; // 输出:true
字典序比较 "apple" 和 "apricot"比到下标 2 就能分出胜负
"apple"
a
[0]
p
[1]
p
[2]
l
[3]
e
[4]
"apricot"
a
[0]
p
[1]
r
[2]
i
[3]
c
[4]
o
[5]
t
[6]
从左到右逐个字符比:下标 [0] 都是 a,[1] 都是 p,到了 [2],p(ASCII 112)和 r(ASCII 114)不一样,p 更小,所以 "apple" < "apricot"。只要找到第一个不同的字符,就立刻决出胜负,后面的字符不用再看了(对应规则 ②)。
前缀的情况:比较 "abc" 和 "abcd"短的先结束就更小
"abc"
a
[0]
b
[1]
c
[2]
"abcd"
a
[0]
b
[1]
c
[2]
d
[3]
前 3 个字符 a、b、c 完全相同,但 "abc" 到这里就结束了,而 "abcd" 还多出一个 d。没有哪个字符更大,只能比长度:较短的更小,所以 "abc" < "abcd"(对应规则 ③)。

“谁的字符更小”看的是字符的 ASCII 码。你只需要记住下面这条大小规律:

字符类型范围ASCII 码
数字字符'0' ~ '9'48 ~ 57
大写字母'A' ~ 'Z'65 ~ 90
小写字母'a' ~ 'z'97 ~ 122
📐
大小规律:数字 < 大写字母 < 小写字母。所以 "Zebra" < "apple" 是 true——大写 Z(90)比小写 a(97)还小。如果想“不区分大小写”地比较,需要先把两个字符串都转成小写(或都转成大写)再比。
⚠️
数字字符串也是按字典序比的:"10" < "9" 结果是 true!因为比的是第一个字符:'1'(49)小于 '9'(57),根本不会去看后面的 0。如果要比较数字的大小,需要先用 stoi 转成整数(见 11.1.6)再比。
11.1.4 输入与输出

输出很简单:cout << s; 就能把字符串完整输出。读入字符串有两种常见方式:只要一个单词用 cin >>,要一整行(包括空格)用 getline()。两者混用时有一个经典的坑,一定要小心。

C++ · cin 与 getline 的区别
1string word, line;
2
3// 情况一:读一个单词(遇到空格、Tab 或换行就停)
4cin >> word; // 输入 "Hello World" → word 得到 "Hello"
5
6// 情况二:读一整行(包括空格,直到换行)
7getline(cin, line); // 输入 "Hello World" → line 得到 "Hello World"
8
9// ⚠️ 混用时的大坑!
10int n;
11cin >> n; // 输入 "42\n",读到 42,但缓冲区还剩一个 '\n'
12cin.ignore(); // 🔑 把残留的换行符清掉
13getline(cin, line); // 现在才能正常读下一行
📖
为什么混用会出问题?
cin >> n 读取完数字后,你按下的回车键(\n)会残留在输入缓冲区里。紧接着 getline 一上来就看到了这个换行符,以为“哦,这行结束了”,于是直接返回一个空字符串——看起来就像程序“跳过”了输入。

解决办法:在 cin 之后、getline 之前,加一句 cin.ignore(),把残留的换行符清掉即可。
💡
选择指南:
· 只读单个单词(无空格)→ cin >> str;
· 需要读含空格的整行 → getline(cin, str);
· 混用 cin 和 getline 时,务必先清理残留的换行符:用 cin.ignore(),或者多读一次 getline(见下面)

🧹 另一种清理办法:多读一次 getline

如果数字后面不小心多打了空格,cin.ignore() 只能丢掉一个字符,清不干净。还有一个更省心的办法:把残留的内容也当成“一行”,多调用一次 getline 把它读走、扔掉。比如输入是这样两行(第一行 42 后面多打了两个空格):

C++ · 用 getline 清理多余的空格和换行符
1// 输入的两行内容:
2// 第 1 行:42(后面还有两个空格)
3// 第 2 行:asdfg
4int n;
5string s;
6cin >> n; // 读到 42,缓冲区里还剩下“两个空格 + 换行符”
7getline(cin, s); // 把这一行剩下的内容整个读走(s 暂时是两个空格,没用,之后会被覆盖)
8getline(cin, s); // 这一次才读到真正想要的下一行
9cout << n << endl; // 输出:42
10cout << s << endl; // 输出:asdfg
📖
为什么这样可行?getline 会一直读到换行符为止,并且把换行符也一起“吃掉”。所以第一个 getline 不管这一行剩下多少个空格、有没有内容,都会把它整行读走;第二个 getline 就能从下一行的开头开始读了。
就算第一行只有 42、后面什么都没有,这个办法也同样有效(第一个 getline 只是读到了一个空字符串)。第一个 getline 读到的内容没有用,可以随便存进 s,反正后面马上会被第二次读取覆盖。
清理办法能清掉多少需要的头文件
cin.ignore()只丢掉 1 个字符(遇到多余空格就清不干净)不需要
多读一次 getline(cin, s)丢掉这一行剩下的全部内容不需要
cin.ignore(numeric_limits<streamsize>::max(), '\n')丢掉这一行剩下的全部内容<limits>
🔧
进阶(可以先跳过):cin.ignore 的完整写法。如果你更习惯用 cin.ignore,可以这样写,效果和多读一次 getline 一样:
cin.ignore(numeric_limits<streamsize>::max(), '\n');
意思是“一直丢,直到丢掉一个换行符为止”,把这一行剩下的内容全部清干净。使用它需要引入 <limits> 头文件(万能头文件已经包含)。
11.1.5 访问字符

把 string 想象成一排编了号的格子,每个格子放一个字符。编号(下标)从 0 开始,所以长度为 5 的字符串,最后一个字符的下标是 4,而不是 5。要访问第 i 个字符,有两种写法:s[i] 和 s.at(i)。

字符串 "Hello" 的内存格子size() = 5
H
[0]
e
[1]
l
[2]
l
[3]
o
[4]
\0
[5]
size() / length() 只统计这 5 个真正的字符,不包含 \0。\0 不是字符串内容的一部分,只是 C++11 起为了兼容 C 语言接口而在末尾隐藏提供的一个只读"哨兵"——可以用 s[s.size()] 读到,但不能写入,也不计入长度。
C++ · 访问与修改字符
1string s = "Hello";
2cout << s[0] << endl; // 输出:H,下标从 0 开始
3cout << s[1] << endl; // 输出:e
4cout << s.at(4) << endl; // 输出:o,at() 会检查有没有越界
5
6char c3 = s.front(); // 'H',第一个字符
7char c4 = s.back(); // 'o',最后一个字符
8
9s[0] = 'h'; // 通过下标可以直接修改字符
10cout << s << endl; // 输出:hello
方式建议
[]更快但不安全,越界访问是未定义行为(程序可能崩溃,也可能“看起来正常”但数据已经坏了)
at()更安全但稍慢,越界时会抛出异常提醒你
🎯
建议:竞赛中确定不越界时用 [],调试阶段或不确定时用 at()。如果程序莫名其妙崩溃或结果乱七八糟,先检查下标有没有越界。

🔁 遍历:把每个字符都处理一遍

遍历有三种常用写法:下标循环(需要知道当前是第几个时用)、范围 for 循环(只关心每个字符本身时用,写起来最简洁),以及迭代器(把字符串当成一排格子,用“书签”一格一格往后移)。最后再看一个在遍历时修改字符的技巧。

C++ · 遍历字符串
1string s = "Hello";
2
3// 写法一:下标循环,i 就是当前字符的下标
4for (int i = 0; i < (int)s.size(); i++)
5{
6 cout << i << ":" << s[i] << " "; // 输出:0:H 1:e 2:l 3:l 4:o
7}
8cout << endl;
9
10// 写法二:范围 for 循环,c 依次是每个字符(得到的是拷贝,改 c 不会影响 s)
11for (char c : s)
12{
13 cout << c << " "; // 输出:H e l l o
14}
15cout << endl;
16
17// 写法三:迭代器。it 像一个“书签”,*it 就是书签所在位置的字符
18for (auto it = s.begin(); it != s.end(); ++it)
19{
20 cout << *it << " "; // 输出:H e l l o
21}
22cout << endl;
23
24// 修改技巧:想在遍历时修改原字符串,范围 for 里要加 &(引用,直接操作原来的格子)
25for (char& c : s)
26{
27 c = toupper(c); // 把每个字符变成大写,需要 <cctype>
28}
29cout << s << endl; // 输出:HELLO
📖
迭代器是什么?s.begin() 指向第一个字符,s.end() 指向最后一个字符后面的位置(表示“已经走完了”),++it 把书签往后移一格,*it 读出当前格子里的字符。对 string 来说,下标循环和范围 for 已经够用;但迭代器是 STL 所有容器(vector、set……)通用的遍历方式,所以先在这里认识一下,更详细的解释见 11.1.9。
⚠️
写下标循环时记得转成 int:循环条件写 i < (int)s.size(),原因见 11.1.2 的“坑 1”。如果写成 i <= s.size() - 1,遇到空字符串就会因为 size() - 1 变成超级大的数而越界。
11.1.6 字符串与数字互转

程序从键盘读到的内容,经常是以“字符串”的形式存在的;但要做加减乘除,就需要数值类型。反过来,想把计算结果拼到一句话里输出,也需要先把数字变成字符串。C++11 提供了两组现成的工具来做这两件事。

🔢 字符串 → 数字:stoi 系列

函数功能目标类型示例
stoi()string 转 intintstoi("123") → 123
stol()string 转 longlongstol("123") → 123L
stoll()string 转 long longlong longstoll("123") → 123LL
stof()string 转 floatfloat(单精度小数)stof("3.14") → 3.14f
stod()string 转 doubledouble(双精度小数)stod("3.14") → 3.14
C++ · stoi 系列:字符串转数字
1string s1 = "123";
2int num1 = stoi(s1);
3cout << num1 + 1 << endl; // 输出:124,说明已经变成真正的数字了
4
5string s2 = "1234567890123"; // 超过了 int 的范围,要用 stoll
6long long num2 = stoll(s2);
7cout << num2 << endl; // 输出:1234567890123
8
9string s3 = "2.718281828";
10double num3 = stod(s3);
11cout << num3 << endl; // 输出:2.71828(cout 默认只显示 6 位有效数字)
12
13// 转换后的数值可以直接参与运算
14string sa = "100", sb = "200";
15int sum = stoi(sa) + stoi(sb);
16cout << sum << endl; // 输出:300
⚠️
注意:stoi/stod 在转换失败时(比如传入的字符串根本不是数字,像 stoi("abc"))会抛出异常,如果程序没有处理这个异常,会直接崩溃退出。数字太大装不进 int(如 stoi("99999999999"))同样会抛出异常,这时要换成 stoll。
另外有一个容易忽略的细节:stoi("12abc") 不会报错,它会读到不能再读为止,得到 12。
在竞赛中,如果题目保证输入格式一定合法,可以放心直接使用;但如果输入内容可能不合法(比如处理用户输入或格式不确定的外部数据),就需要额外用 try-catch 处理异常,或者改用更稳健的解析方式(比如 stringstream)。

🔤 数字 → 字符串:to_string

to_string(数值) 可以把 int、long long、double 等各种数值类型转成字符串,编译器会自动选对的版本。转换之后就可以用 + 和别的字符串拼在一起了。

C++ · to_string:数字转字符串
1int a = 123;
2string s1 = to_string(a);
3cout << s1 << endl; // 输出:123
4
5double b = 3.14159;
6string s2 = to_string(b);
7cout << s2 << endl; // 输出:3.141590(小数点后固定 6 位)
8
9int score = 95;
10string msg = "你的成绩是:" + to_string(score) + " 分";
11cout << msg << endl; // 输出:你的成绩是:95 分
12
13// 往返转换:字符串 → 数字 → 运算 → 字符串
14string original = "256";
15int num = stoi(original); // 字符串 → 数字
16int doubled = num * 2; // 数值运算
17string result = to_string(doubled); // 数字 → 字符串
18cout << "256 的两倍是:" << result << endl; // 输出:256 的两倍是:512
📖
小数转字符串的精度:to_string 转小数时,小数点后固定保留 6 位(所以 3.14159 变成了 "3.141590")。如果想自己控制保留几位,不要用 to_string,改用 cout << fixed << setprecision(2) 直接输出(见 3.1 节)。

🔟 一个超实用的小技巧:单个数字字符 ↔ 数字

处理“一位一位的数字”时(比如高精度计算),经常要把字符串里的一个字符 '7' 变成数字 7。办法很简单:减去字符 '0'。因为数字字符的 ASCII 码是连续的('0' 是 48,'7' 是 55,相差正好 7)。反过来,加上 '0' 就能把数字变回字符。

C++ · 数字字符和数字互转
1char c = '7';
2int d = c - '0'; // '7' - '0' = 55 - 48 = 7
3cout << d + 1 << endl; // 输出:8
4
5int x = 5;
6char ch = x + '0'; // 5 + 48 = 53,也就是字符 '5'
7cout << ch << endl; // 输出:5
8
9// 用它统计一个数字字符串的各位数字之和
10string num = "12345";
11int sum = 0;
12for (char c : num)
13{
14 sum += c - '0';
15}
16cout << sum << endl; // 输出:15
转换方向函数示例
字符串 → 整数stoi() / stol() / stoll()stoi("123") → 123
字符串 → 小数stof() / stod()stod("3.14") → 3.14
数值 → 字符串to_string()to_string(123) → "123"
数字字符 ↔ 数字c - '0' / d + '0''7' - '0' → 7
11.1.7 子串操作:查找、截取、插入、删除与替换

一个字符串的“一部分”叫子串(比如 "World" 是 "Hello World" 的子串)。对子串可以做 5 件事:查找(find)、截取(substr)、插入(insert)、删除(erase)、替换(replace)。其中 insert、erase、replace 会直接修改原字符串;find 和 substr 不会改动原字符串。

💡
最重要的套路:先 find 定位,再动手。实际写程序时,这 5 个操作经常组合起来用:先用 find 找到目标在哪,再用 substr 截下来、用 erase 删掉、用 replace 换掉,或者用 insert 在那里插入新内容。它们都用下标来指定位置,下标从 0 开始。

🔎 查找子串:find 系列

s.find(子串) 从左往右找,返回子串第一次出现的位置——也就是子串第一个字符的下标。找不到就返回 string::npos(意思是“没找到”)。除了 find,还有几个兄弟函数:

函数查找方向返回什么示例
find(str)从左向右,首次出现子串首字符的下标s.find("abc")
find(str, pos)从下标 pos 开始向右子串首字符的下标s.find("abc", 2)
rfind(str)从右向左,最后一次出现子串首字符的下标s.rfind("abc")
find_first_of(chars)从左向右,chars 里的任意一个字符第一个匹配字符的下标s.find_first_of("aeiou")
find_last_of(chars)从右向左,chars 里的任意一个字符最后一个匹配字符的下标s.find_last_of("aeiou")
string::npos——表示“没找到”的特殊值if (pos != string::npos)

以上所有查找函数,要找的内容既可以是字符串 "abc",也可以是单个字符 'a'。

find 与 substr 配合:定位 + 截取s = "Hello World"
H
[0]
e
[1]
l
[2]
l
[3]
o
[4]
␣
[5]
W
[6]
o
[7]
r
[8]
l
[9]
d
[10]
s.find("World") 返回 6("World" 的第一个字符 W 的下标)。再用 s.substr(6, 5):从下标 6 开始,截取 5 个字符,就得到高亮的 "World"。空格也是一个字符(图中用 ␣ 表示),所以 H 到 o 加空格正好占了下标 0~5。
C++ · find 系列
1string s = "Hello World, Hello C++";
2
3size_t pos = s.find("Hello"); // 从左往右找,返回首次出现的下标
4cout << pos << endl; // 输出:0
5
6pos = s.find("Hello", 1); // 从下标 1 开始往后找,跳过开头那个 Hello
7cout << pos << endl; // 输出:13
8
9pos = s.find('W'); // 也可以找单个字符
10cout << pos << endl; // 输出:6
11
12pos = s.find("xyz"); // 找不到,返回 string::npos
13if (pos == string::npos)
14{
15 cout << "没找到 xyz" << endl;
16}
17
18pos = s.rfind("o"); // 从右往左找,返回最后一次出现的下标
19cout << pos << endl; // 输出:17
20
21pos = s.find_first_of("aeiouAEIOU"); // 找任意一个元音字母第一次出现的位置
22cout << pos << "(字符 " << s[pos] << ")" << endl; // 输出:1(字符 e)

🔁 综合:找出所有出现的位置

想找“所有”出现的位置,就用 while 循环:每找到一个,就从它的下一个位置继续往后找,直到返回 npos 为止。

C++ · 找出所有 'l' 的位置
1string s = "Hello World, Hello C++";
2
3cout << "所有 l 的位置:";
4size_t pos = s.find('l');
5while (pos != string::npos)
6{
7 cout << pos << " ";
8 pos = s.find('l', pos + 1); // 从刚找到的位置的下一个位置继续找
9}
10cout << endl; // 输出:2 3 9 15 16
📖
string::npos 是什么?你可以把它理解为"无效位置"。当 find() 找不到你要的东西时,就返回 string::npos 来告诉你"抱歉,没找到"。使用时只需要记住:拿 find() 的返回值和 string::npos 比较,相等就说明没找到。
find 的返回值请用 size_t(或 auto)来接收,并用 pos != string::npos 判断成功与否。不要写 pos == -1:虽然 npos 的值碰巧等价于 -1,但这样写不规范,换个写法就可能出错。

✂️ 截取子串:substr

s.substr(起始位置, 截取长度) 从原字符串中“剪”出一段,生成一个新的字符串,原字符串保持不变。如果省略长度,就一直截到末尾。

C++ · substr 截取
1string s = "Hello World";
2
3string sub = s.substr(6, 5); // 从下标 6 开始,截取 5 个字符
4cout << sub << endl; // 输出:World
5
6cout << s.substr(6) << endl; // 省略长度,截到末尾,输出:World
7cout << s.substr(0, 5) << endl; // 前 5 个字符,输出:Hello
8cout << s.substr(s.size() - 5) << endl; // 后 5 个字符,输出:World
9cout << s << endl; // 原串没变,输出:Hello World
💡
常用模式:s.substr(0, n) 取前 n 个字符,s.substr(s.size() - n) 取后 n 个字符。如果长度写得比剩下的字符还多,会自动截到末尾,不会报错;但起始位置比字符串长度还大就会抛出异常。

🧩 实战:find + substr 拆分字符串

下面两个例子都是“先 find 找分隔符,再 substr 截取”的经典用法:提取邮箱的用户名和域名,以及把用逗号隔开的内容拆开。

C++ · 用 find + substr 拆分字符串
1// 例一:提取邮箱的用户名和域名
2string email = "student@school.edu.cn";
3size_t atPos = email.find('@'); // '@' 的位置是 7
4string username = email.substr(0, atPos); // 前面部分:student
5string domain = email.substr(atPos + 1); // 后面部分:school.edu.cn(+1 是为了跳过 '@' 本身)
6cout << "用户名:" << username << ",域名:" << domain << endl;
7
8// 例二:拆分逗号分隔的字符串
9string csv = "apple,banana,orange";
10size_t p1 = csv.find(','); // 第一个逗号,下标 5
11size_t p2 = csv.find(',', p1 + 1); // 第二个逗号,下标 12
12cout << csv.substr(0, p1) << endl; // 输出:apple
13cout << csv.substr(p1 + 1, p2 - p1 - 1) << endl; // 输出:banana
14cout << csv.substr(p2 + 1) << endl; // 输出:orange
📖
怎么算“中间那段”的长度?第二个单词 banana 从 p1 + 1 开始,到 p2(第二个逗号)前面结束,所以长度是 p2 - p1 - 1。可以自己验算:12 - 5 - 1 = 6,banana 正好 6 个字母。

📝 插入、删除与替换:insert、erase、replace

这三个操作都会直接修改原字符串,修改后字符串的长度会自动变长或变短。先看一张图,了解它们各自做了什么:

insert / erase / replace 前后对比修改后长度自动变化
insert(1, 3, '-') 修改前
A
[0]
B
[1]
C
[2]
修改后:在下标 1 之前插入 3 个 '-'
A
[0]
-
[1]
-
[2]
-
[3]
B
[4]
C
[5]
erase(2, 3) 修改前
A
[0]
B
[1]
C
[2]
D
[3]
E
[4]
F
[5]
G
[6]
修改后:从下标 2 起删掉 3 个字符(C D E),后面的字符自动往前补
A
[0]
B
[1]
F
[2]
G
[3]
replace(7, 4, "C++") 修改前
I
[0]
␣
[1]
l
[2]
i
[3]
k
[4]
e
[5]
␣
[6]
J
[7]
a
[8]
v
[9]
a
[10]
修改后:"Java"(4 个字符)被换成 "C++"(3 个字符),长度可以不一样
I
[0]
␣
[1]
l
[2]
i
[3]
k
[4]
e
[5]
␣
[6]
C
[7]
+
[8]
+
[9]
粉色虚线的格子是被删除 / 被替换掉的字符,蓝色的格子是新插入的字符。注意:insert(pos, ...) 是插在下标 pos 的字符前面,原来 pos 及之后的字符都往后挪。

insert:插入。s.insert(位置, 内容) 在指定位置之前插入内容;s.insert(位置, n, 字符) 一次插入 n 个相同的字符。

C++ · insert 插入
1string s1 = "Hello World";
2s1.insert(6, "Beautiful "); // 在下标 6 之前插入
3cout << s1 << endl; // 输出:Hello Beautiful World
4
5string s2 = "World";
6s2.insert(0, "Hello "); // 在最开头插入
7cout << s2 << endl; // 输出:Hello World
8
9string s3 = "ABC";
10s3.insert(1, 3, '-'); // 在下标 1 之前插入 3 个 '-'
11cout << s3 << endl; // 输出:A---BC
12
13// 应用:把手机号格式化成 138-1234-5678
14string phone = "13812345678";
15phone.insert(3, "-"); // 138-12345678
16phone.insert(8, "-"); // 138-1234-5678(第一次插入后,后面的字符整体后移了一位)
17cout << phone << endl;

erase:删除。s.erase(起始位置, 删除长度) 删掉指定范围的字符;省略长度就删到末尾;删单个字符就写 s.erase(pos, 1)。

C++ · erase 删除
1string s4 = "ABCDEFG";
2s4.erase(2, 3); // 从下标 2 开始,删掉 3 个字符(C D E)
3cout << s4 << endl; // 输出:ABFG
4
5string s5 = "Hello World";
6s5.erase(5); // 省略长度:从下标 5 一直删到末尾
7cout << s5 << endl; // 输出:Hello
8
9// 结合 find:删掉指定内容
10string s6 = "Hello World";
11size_t pos = s6.find("Wo");
12if (pos != string::npos)
13{
14 s6.erase(pos, 2); // 删掉 "Wo"
15}
16cout << s6 << endl; // 输出:Hello rld
17
18// 循环 find + erase:删掉全部空格
19string s7 = "a b c d e";
20pos = s7.find(' ');
21while (pos != string::npos)
22{
23 s7.erase(pos, 1);
24 pos = s7.find(' ', pos); // 注意:不用 +1,因为删掉空格后,后面的字符已经往前补位了
25}
26cout << s7 << endl; // 输出:abcde

replace:替换。s.replace(起始位置, 被替换的长度, 新内容) 把指定范围的字符换成新内容,新内容和被换掉的部分长度可以不一样,字符串总长度会自动调整。新内容写空字符串 "",效果就等于 erase。

C++ · replace 替换
1string s8 = "I like Java";
2s8.replace(7, 4, "C++"); // 把下标 7 起的 4 个字符 "Java" 换成 "C++"
3cout << s8 << endl; // 输出:I like C++
4
5string s9 = "ABCDEFG";
6s9.replace(2, 3, "XYZ"); // 把 "CDE" 换成 "XYZ"
7cout << s9 << endl; // 输出:ABXYZFG
8
9string s10 = "ABxxxCD";
10s10.replace(2, 3, ""); // 替换成空 = 删除
11cout << s10 << endl; // 输出:ABCD
12
13// 循环 find + replace:把所有 "one" 换成 "1"
14string s11 = "one two one two";
15size_t pos = s11.find("one");
16while (pos != string::npos)
17{
18 s11.replace(pos, 3, "1");
19 pos = s11.find("one", pos + 1);
20}
21cout << s11 << endl; // 输出:1 two 1 two
🚫
“全部替换”小心死循环:上面的写法里,新内容 "1" 里不包含要找的 "one",所以 pos + 1 没问题。但如果新内容里又包含了要找的内容——比如把 "a" 替换成 "aa"——每替换一次就会产生一个新的 "a",用 pos + 1 会一直找到刚替换出来的,永远停不下来。
正确做法是让 pos 直接跳过新内容:pos += 新内容的长度;(上面的例子里新内容长度是 1,所以 pos + 1 恰好可用)。
⚠️
修改操作的三个通用注意事项:
1. insert、erase、replace 都直接修改原字符串。如果之后还需要原来的内容,请先复制一份:string backup = s;。
2. 要保证起始位置不越界(不能大于字符串长度),否则程序会抛出 out_of_range 异常。
3. 插入、删除、替换之后,字符串的长度和后面字符的下标都会变化。在循环里反复修改时,要特别注意下标有没有“错位”。

🧪 综合实战:五个操作一起用

任务:处理字符串 " Hello World! "(注意两头各有一个空格)。依次完成:① 查找 World 的位置;② 截取出 World!;③ 在 World 后面插入 " C++";④ 删除所有空格;⑤ 把 Hello 替换成 Hi。

C++ · 五大操作组合实战
1#include <iostream>
2#include <string>
3using namespace std;
4
5int main()
6{
7 string s = " Hello World! ";
8 cout << "原始字符串:\"" << s << "\"" << endl;
9
10 // 1. 查找 "World" 的位置(开头有个空格,所以 W 的下标是 7)
11 size_t worldPos = s.find("World");
12 cout << "World 的位置:" << worldPos << endl; // 输出:7
13
14 // 2. 截取 "World!"(find + substr 组合)
15 string world = s.substr(worldPos, 6); // "World!" 共 6 个字符
16 cout << "截取结果:" << world << endl; // 输出:World!
17
18 // 3. 在 "World" 后面插入 " C++"(World 占 5 个字符,所以插在 worldPos + 5)
19 s.insert(worldPos + 5, " C++");
20 cout << "插入后:" << s << endl; // 输出: Hello World C++!
21
22 // 4. 循环 find + erase:删除所有空格
23 size_t spacePos = s.find(' ');
24 while (spacePos != string::npos)
25 {
26 s.erase(spacePos, 1);
27 spacePos = s.find(' ', spacePos); // 从当前位置继续,不用 +1
28 }
29 cout << "去空格后:" << s << endl; // 输出:HelloWorldC++!
30
31 // 5. 把 "Hello" 替换成 "Hi"(find + replace 组合)
32 size_t helloPos = s.find("Hello");
33 if (helloPos != string::npos)
34 {
35 s.replace(helloPos, 5, "Hi");
36 }
37 cout << "替换后:" << s << endl; // 输出:HiWorldC++!
38
39 return 0;
40}
操作写法效果是否修改原串
查找s.find(str)返回子串首次出现的下标,找不到返回 npos❌ 不修改
截取s.substr(pos, len)返回截取出来的新字符串❌ 不修改
插入s.insert(pos, str)在下标 pos 之前插入 str✅ 原地修改
删除s.erase(pos, len)删除从 pos 开始的 len 个字符✅ 原地修改
替换s.replace(pos, len, str)把从 pos 开始的 len 个字符换成 str✅ 原地修改
🎯
核心规律:find 定位 → 确定位置 → substr 截取 / erase 删除 / replace 替换 / insert 插入。五个操作都用下标定位,下标从 0 开始。
11.1.8 排序与反转:sort 与 reverse

字符串可以看成“一排字符”,所以 <algorithm> 里处理序列的工具,也能直接用在 string 上。最常用的两个是 sort(排序)和 reverse(反转)。它们都直接修改原字符串。

🔀 sort:给字符排队

sort(s.begin(), s.end()) 把字符串里的字符按 ASCII 码从小到大排好。想从大到小,在后面加一个 greater<char>();或者用反向迭代器 sort(s.rbegin(), s.rend())。

C++ · sort 排序字符
1#include <algorithm> // sort 和 reverse 都需要这个头文件
2#include <functional> // greater 需要这个头文件
3
4string s1 = "dcba";
5sort(s1.begin(), s1.end()); // 升序
6cout << s1 << endl; // 输出:abcd
7
8string s2 = "hello";
9sort(s2.begin(), s2.end(), greater<char>()); // 降序
10cout << s2 << endl; // 输出:ollhe
11
12// 大小写混合:大写字母的 ASCII 比小写小,所以大写排在前面
13string s3 = "CbAd";
14sort(s3.begin(), s3.end());
15cout << s3 << endl; // 输出:ACbd
⚠️
排序是“不可逆”的:排完之后,原来的顺序就没有了。如果之后还要用原字符串,请先复制一份:string backup = s;。排序规则就是 11.1.3 讲过的 ASCII 大小规律:数字 < 大写字母 < 小写字母。

↩️ reverse:把字符串倒过来

reverse(s.begin(), s.end()) 把整个字符串首尾颠倒。只想倒前面一部分,就把结束位置改成 s.begin() + n,只会反转前 n 个字符。

C++ · reverse 反转
1string s4 = "ABCDEFG";
2reverse(s4.begin(), s4.end());
3cout << s4 << endl; // 输出:GFEDCBA
4
5string s5 = "ABCDEFG";
6reverse(s5.begin(), s5.begin() + 3); // 只反转前 3 个字符
7cout << s5 << endl; // 输出:CBADEFG

🪞 应用:判断回文串

回文串就是正着读和倒着读都一样的字符串,比如 "level"、"racecar"。判断方法很直接:把字符串复制一份,反转之后和原来的比一比,相等就是回文。

C++ · 用 reverse 判断回文
1string s = "level";
2string rev = s; // 先复制一份
3reverse(rev.begin(), rev.end()); // 把复制品反转
4cout << s << (s == rev ? " 是" : " 不是") << "回文串" << endl; // 输出:level 是回文串

👉👈 手动反转:双指针法(理解原理)

reverse 的原理其实很简单,就是首尾对称交换:一个指针 left 从最左边出发,一个指针 right 从最右边出发,交换它们指向的字符,然后各自向中间靠拢,直到两个指针碰头为止。这叫“双指针”思想,后面的算法篇还会反复用到。

双指针反转 "12345"只需交换 2 次
第 1 步:left = 0,right = 4,交换 '1' 和 '5' → 52341
1
[0]
2
[1]
3
[2]
4
[3]
5
[4]
第 2 步:left = 1,right = 3,交换 '2' 和 '4' → 54321
5
[0]
2
[1]
3
[2]
4
[3]
1
[4]
第 3 步:left = 2,right = 2,两个指针碰头(left < right 不成立),结束
5
[0]
4
[1]
3
[2]
2
[3]
1
[4]
每次交换都能确定首尾两个位置,所以长度为 n 的字符串只需要交换 n/2 次,时间复杂度是 O(n)。因为是在原字符串上“原地”交换,不需要额外开辟新字符串,所以空间复杂度是 O(1)。
C++ · 双指针手动反转
1string s = "12345";
2int left = 0, right = (int)s.size() - 1; // 左指针指向开头,右指针指向末尾
3while (left < right)
4{
5 swap(s[left], s[right]); // 交换两个指针指向的字符
6 left++; // 左指针向右移
7 right--; // 右指针向左移
8}
9cout << s << endl; // 输出:54321
函数功能是否修改原串需要头文件
sort(s.begin(), s.end())升序排列字符✅ 原地修改<algorithm>
reverse(s.begin(), s.end())颠倒字符顺序✅ 原地修改<algorithm>
11.1.9 迭代器遍历

把 string 想象成一排连续的格子,每个格子里放一个字符。迭代器就像夹在格子之间的"书签"——它记住了你当前所在的位置。你可以用 *it 读取当前格子的字符,用 ++it 把书签往后移一格。(迭代器的概念在 11.0 里介绍过,11.1.5 里你也已经见过它的写法,这一节再把含义讲清楚。)

C++ · 用迭代器和范围 for 遍历
1string s = "Hi";
2// begin() 指向第一个字符,end() 指向尾后位置
3for (auto it = s.begin(); it != s.end(); ++it)
4{
5 cout << *it << " "; // 输出:H i
6}
7
8// C++11 范围 for 循环(底层也是用迭代器)
9for (char c : s)
10{
11 cout << c; // 输出:Hi
12}
📌
小贴士:s.begin() 返回指向第一个字符的迭代器,s.end() 返回指向最后一个字符后面的迭代器——它不指向任何字符,只表示"已经遍历完了"。前面 sort、reverse 里写的 s.begin()、s.end(),就是在告诉函数“处理从头到尾这一段”。
📋 string 常用操作速查表

把本节出现过的方法按用途归类汇总,写代码时可以直接当参考卡用。

方法分类作用
s + s2 / s += s2拼接拼接两个字符串
== / != / < / >比较按字典序比较
s[i]访问下标访问,不检查越界,速度快
s.at(i)访问下标访问,越界抛异常,更安全
s.front() / s.back()访问第一个 / 最后一个字符
s.append(t)修改在末尾追加字符串
s.push_back(c) / s.pop_back()修改末尾追加 / 删除一个字符
s.insert(pos, t)修改在 pos 处插入字符串
s.erase(pos, len)修改删除 pos 起的 len 个字符(省略 len 则删到末尾)
s.replace(pos, len, t)修改替换 pos 起的 len 个字符为 t
s.find(t)查找查找 t 首次出现的位置,找不到返回 string::npos
s.rfind(t)查找从后往前查找 t 最后一次出现的位置
s.find_first_of(chars)查找查找 chars 中任意字符首次出现的位置
s.find_last_of(chars)查找查找 chars 中任意字符最后一次出现的位置
s.substr(pos, len)截取从 pos 开始截取 len 个字符(省略 len 则截到末尾)
to_string(num)转换数字 → 字符串
stoi(s) / stol(s) / stoll(s)转换字符串 → int / long / long long
stof(s) / stod(s)转换字符串 → float / double
sort(s.begin(), s.end())排序按 ASCII 码升序排列(需要 <algorithm>)
reverse(s.begin(), s.end())反转颠倒字符顺序(需要 <algorithm>)
s.size() / s.length() / s.empty()容量长度 / 长度 / 判断是否为空
s.clear()容量清空字符串
s.begin() / s.end()遍历正向迭代器范围
s1.swap(s2)其它交换两个字符串的全部内容
🎯 本节小结
🎯
这一节你学会了:
1. string 是 C++ 处理字符串的核心类型,比 char 数组更安全、更方便;用 = 赋值,用 .size() / .length() 获取长度。
2. 用 +、+= 拼接,用 ==、<、> 按字典序比较。注意两个双引号字符串不能直接相加,size() 是无符号数。
3. cin 读不含空格的内容,getline() 读整行;混用时要先清掉残留的换行符(cin.ignore(),或者多读一次 getline)。
4. 用 [i] 或 .at(i) 访问、修改字符,下标从 0 开始,用范围 for 或下标循环遍历。
5. 字符串 → 数字用 stoi / stoll / stod,数字 → 字符串用 to_string,数字字符和数字之间用 - '0' / + '0'。
6. 子串五大操作:find 找位置、substr 截取、insert 插入、erase 删除、replace 替换,常常用 find 定位后配合使用。
7. sort 给字符排序,reverse 反转字符顺序,两者都直接修改原字符串,需要 <algorithm>。