C++ 标准库提供的文本处理类型——比 C 风格字符数组更安全、更灵活,是处理字符串的首选工具。
C++ 标准库提供了 string 类型,专门用来处理文字信息。你可以把它想象成一条可以自动变长的纸带:纸带上一格放一个字符,你往后面接多少字,它就自己变长多少,不用你操心“够不够放”。使用前需引入 <string> 头文件(如果你用的是万能头文件 <bits/stdc++.h>,就不用再单独写了)。
先分清两个容易混的东西: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。下面这个完整的小程序展示了 string 的几种“出生方式”。后面的代码片段为了节省篇幅,都省略了 #include 和 main(),你把它们放进自己的 main() 里就能运行。
| 1 | #include <iostream> |
| 2 | #include <string> // 必须包含这个头文件 |
| 3 | using namespace std; |
| 4 | |
| 5 | int 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() 把字符串清空。
| 1 | string s = "Hello"; |
| 2 | cout << s.size() << endl; // 输出:5 |
| 3 | cout << s.length() << endl; // 输出:5,和 size() 完全一样 |
| 4 | |
| 5 | string t; // 空字符串 |
| 6 | cout << t.empty() << endl; // 输出:1(true),说明 t 是空的 |
| 7 | |
| 8 | s.clear(); // 清空 s |
| 9 | cout << s.size() << endl; // 输出:0 |
| 10 | |
| 11 | string a = "left", b = "right"; |
| 12 | a.swap(b); // 交换两个字符串的全部内容(速度极快) |
| 13 | cout << a << " " << b << endl; // 输出:right left |
size() 返回的是“无符号整数”。s.size() 的类型是 size_t(无符号整数)。如果 s 是空字符串,s.size() - 1 不会得到 -1,而是变成一个超级大的数(约 1.8×1019),用在循环里会直接越界崩溃。int 比较时,先把它转成 int:int n = (int)s.size();,这样 n - 1 就能正常得到 -1 了。string 里占好几个字节:在 UTF-8 编码下占 3 个,在 GBK 编码下占 2 个。所以 string s = "你好"; 的 s.size() 是 6(UTF-8)或 4(GBK),不是 2。size() 数的是字节,不是“字”。对中文字符串用 s[i] 逐个访问会把一个汉字拆成碎片。竞赛题里的字符串基本都是英文字母和数字,一般不会遇到这个问题;但自己写带中文的程序时要记得这一点。🔗 拼接:+ 和 +=
string 最方便的地方,就是可以像加数字一样“加”字符串:+ 把两个字符串连成一个新的字符串;+= 直接在原字符串末尾追加内容。追加单个字符也一样可以用 +=。
| 1 | string s1 = "Hello"; |
| 2 | string s2 = " World"; |
| 3 | string s3 = s1 + s2; // 用 + 拼接,得到新字符串 |
| 4 | cout << s3 << endl; // 输出:Hello World |
| 5 | |
| 6 | string s4 = "C++"; |
| 7 | s4 += " Fun"; // 用 += 在 s4 末尾追加 |
| 8 | cout << s4 << endl; // 输出:C++ Fun |
| 9 | |
| 10 | string s5 = "ABC"; |
| 11 | s5 += 'D'; // 追加单个字符 |
| 12 | cout << s5 << endl; // 输出:ABCD |
| 13 | |
| 14 | // 连续拼接:从左到右一个一个算 |
| 15 | string s6 = "I" + string(" love ") + string("C++"); |
| 16 | cout << 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() 删掉最后一个字符。
| 1 | string s = "Hello"; |
| 2 | s.append(" World"); // "Hello World"——在末尾追加字符串 |
| 3 | s.push_back('!'); // "Hello World!"——在末尾追加一个字符 |
| 4 | s.pop_back(); // "Hello World"——删除最后一个字符 |
⚖️ 比较:== != < >
string 可以直接用比较运算符:== 判断两个字符串是不是完全一样,!= 判断是不是不一样;<、>、<=、>= 则按字典序比较大小——就像查英语词典:先比第一个字母,谁的字母排在前面谁就“更小”;第一个字母一样,再比第二个……
char 的整数值,通常是 ASCII 码)。== 为 true)。| 1 | cout << boolalpha; // 让 true / false 显示成英文单词,而不是 1 / 0 |
| 2 | |
| 3 | string s1 = "apple", s2 = "apple", s3 = "banana"; |
| 4 | cout << (s1 == s2) << endl; // 输出:true,内容完全一样 |
| 5 | cout << (s1 != s3) << endl; // 输出:true,内容不一样 |
| 6 | cout << (s1 < s3) << endl; // 输出:true,因为 'a' < 'b' |
| 7 | cout << (s3 > s1) << endl; // 输出:true |
| 8 | |
| 9 | // 一方是另一方的前缀时,更短的更小 |
| 10 | string s4 = "abc", s5 = "abcd"; |
| 11 | cout << (s4 < s5) << endl; // 输出:true |
[0] 都是 a,[1] 都是 p,到了 [2],p(ASCII 112)和 r(ASCII 114)不一样,p 更小,所以 "apple" < "apricot"。只要找到第一个不同的字符,就立刻决出胜负,后面的字符不用再看了(对应规则 ②)。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)再比。输出很简单:cout << s; 就能把字符串完整输出。读入字符串有两种常见方式:只要一个单词用 cin >>,要一整行(包括空格)用 getline()。两者混用时有一个经典的坑,一定要小心。
| 1 | string word, line; |
| 2 | |
| 3 | // 情况一:读一个单词(遇到空格、Tab 或换行就停) |
| 4 | cin >> word; // 输入 "Hello World" → word 得到 "Hello" |
| 5 | |
| 6 | // 情况二:读一整行(包括空格,直到换行) |
| 7 | getline(cin, line); // 输入 "Hello World" → line 得到 "Hello World" |
| 8 | |
| 9 | // ⚠️ 混用时的大坑! |
| 10 | int n; |
| 11 | cin >> n; // 输入 "42\n",读到 42,但缓冲区还剩一个 '\n' |
| 12 | cin.ignore(); // 🔑 把残留的换行符清掉 |
| 13 | getline(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 后面多打了两个空格):
| 1 | // 输入的两行内容: |
| 2 | // 第 1 行:42(后面还有两个空格) |
| 3 | // 第 2 行:asdfg |
| 4 | int n; |
| 5 | string s; |
| 6 | cin >> n; // 读到 42,缓冲区里还剩下“两个空格 + 换行符” |
| 7 | getline(cin, s); // 把这一行剩下的内容整个读走(s 暂时是两个空格,没用,之后会被覆盖) |
| 8 | getline(cin, s); // 这一次才读到真正想要的下一行 |
| 9 | cout << n << endl; // 输出:42 |
| 10 | cout << 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> 头文件(万能头文件已经包含)。把 string 想象成一排编了号的格子,每个格子放一个字符。编号(下标)从 0 开始,所以长度为 5 的字符串,最后一个字符的下标是 4,而不是 5。要访问第 i 个字符,有两种写法:s[i] 和 s.at(i)。
size() / length() 只统计这 5 个真正的字符,不包含 \0。\0 不是字符串内容的一部分,只是 C++11 起为了兼容 C 语言接口而在末尾隐藏提供的一个只读"哨兵"——可以用 s[s.size()] 读到,但不能写入,也不计入长度。| 1 | string s = "Hello"; |
| 2 | cout << s[0] << endl; // 输出:H,下标从 0 开始 |
| 3 | cout << s[1] << endl; // 输出:e |
| 4 | cout << s.at(4) << endl; // 输出:o,at() 会检查有没有越界 |
| 5 | |
| 6 | char c3 = s.front(); // 'H',第一个字符 |
| 7 | char c4 = s.back(); // 'o',最后一个字符 |
| 8 | |
| 9 | s[0] = 'h'; // 通过下标可以直接修改字符 |
| 10 | cout << s << endl; // 输出:hello |
| 方式 | 建议 |
|---|---|
| [] | 更快但不安全,越界访问是未定义行为(程序可能崩溃,也可能“看起来正常”但数据已经坏了) |
| at() | 更安全但稍慢,越界时会抛出异常提醒你 |
[],调试阶段或不确定时用 at()。如果程序莫名其妙崩溃或结果乱七八糟,先检查下标有没有越界。🔁 遍历:把每个字符都处理一遍
遍历有三种常用写法:下标循环(需要知道当前是第几个时用)、范围 for 循环(只关心每个字符本身时用,写起来最简洁),以及迭代器(把字符串当成一排格子,用“书签”一格一格往后移)。最后再看一个在遍历时修改字符的技巧。
| 1 | string s = "Hello"; |
| 2 | |
| 3 | // 写法一:下标循环,i 就是当前字符的下标 |
| 4 | for (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 | } |
| 8 | cout << endl; |
| 9 | |
| 10 | // 写法二:范围 for 循环,c 依次是每个字符(得到的是拷贝,改 c 不会影响 s) |
| 11 | for (char c : s) |
| 12 | { |
| 13 | cout << c << " "; // 输出:H e l l o |
| 14 | } |
| 15 | cout << endl; |
| 16 | |
| 17 | // 写法三:迭代器。it 像一个“书签”,*it 就是书签所在位置的字符 |
| 18 | for (auto it = s.begin(); it != s.end(); ++it) |
| 19 | { |
| 20 | cout << *it << " "; // 输出:H e l l o |
| 21 | } |
| 22 | cout << endl; |
| 23 | |
| 24 | // 修改技巧:想在遍历时修改原字符串,范围 for 里要加 &(引用,直接操作原来的格子) |
| 25 | for (char& c : s) |
| 26 | { |
| 27 | c = toupper(c); // 把每个字符变成大写,需要 <cctype> |
| 28 | } |
| 29 | cout << s << endl; // 输出:HELLO |
s.begin() 指向第一个字符,s.end() 指向最后一个字符后面的位置(表示“已经走完了”),++it 把书签往后移一格,*it 读出当前格子里的字符。对 string 来说,下标循环和范围 for 已经够用;但迭代器是 STL 所有容器(vector、set……)通用的遍历方式,所以先在这里认识一下,更详细的解释见 11.1.9。i < (int)s.size(),原因见 11.1.2 的“坑 1”。如果写成 i <= s.size() - 1,遇到空字符串就会因为 size() - 1 变成超级大的数而越界。程序从键盘读到的内容,经常是以“字符串”的形式存在的;但要做加减乘除,就需要数值类型。反过来,想把计算结果拼到一句话里输出,也需要先把数字变成字符串。C++11 提供了两组现成的工具来做这两件事。
🔢 字符串 → 数字:stoi 系列
| 函数 | 功能 | 目标类型 | 示例 |
|---|---|---|---|
| stoi() | string 转 int | int | stoi("123") → 123 |
| stol() | string 转 long | long | stol("123") → 123L |
| stoll() | string 转 long long | long long | stoll("123") → 123LL |
| stof() | string 转 float | float(单精度小数) | stof("3.14") → 3.14f |
| stod() | string 转 double | double(双精度小数) | stod("3.14") → 3.14 |
| 1 | string s1 = "123"; |
| 2 | int num1 = stoi(s1); |
| 3 | cout << num1 + 1 << endl; // 输出:124,说明已经变成真正的数字了 |
| 4 | |
| 5 | string s2 = "1234567890123"; // 超过了 int 的范围,要用 stoll |
| 6 | long long num2 = stoll(s2); |
| 7 | cout << num2 << endl; // 输出:1234567890123 |
| 8 | |
| 9 | string s3 = "2.718281828"; |
| 10 | double num3 = stod(s3); |
| 11 | cout << num3 << endl; // 输出:2.71828(cout 默认只显示 6 位有效数字) |
| 12 | |
| 13 | // 转换后的数值可以直接参与运算 |
| 14 | string sa = "100", sb = "200"; |
| 15 | int sum = stoi(sa) + stoi(sb); |
| 16 | cout << 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 等各种数值类型转成字符串,编译器会自动选对的版本。转换之后就可以用 + 和别的字符串拼在一起了。
| 1 | int a = 123; |
| 2 | string s1 = to_string(a); |
| 3 | cout << s1 << endl; // 输出:123 |
| 4 | |
| 5 | double b = 3.14159; |
| 6 | string s2 = to_string(b); |
| 7 | cout << s2 << endl; // 输出:3.141590(小数点后固定 6 位) |
| 8 | |
| 9 | int score = 95; |
| 10 | string msg = "你的成绩是:" + to_string(score) + " 分"; |
| 11 | cout << msg << endl; // 输出:你的成绩是:95 分 |
| 12 | |
| 13 | // 往返转换:字符串 → 数字 → 运算 → 字符串 |
| 14 | string original = "256"; |
| 15 | int num = stoi(original); // 字符串 → 数字 |
| 16 | int doubled = num * 2; // 数值运算 |
| 17 | string result = to_string(doubled); // 数字 → 字符串 |
| 18 | cout << "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' 就能把数字变回字符。
| 1 | char c = '7'; |
| 2 | int d = c - '0'; // '7' - '0' = 55 - 48 = 7 |
| 3 | cout << d + 1 << endl; // 输出:8 |
| 4 | |
| 5 | int x = 5; |
| 6 | char ch = x + '0'; // 5 + 48 = 53,也就是字符 '5' |
| 7 | cout << ch << endl; // 输出:5 |
| 8 | |
| 9 | // 用它统计一个数字字符串的各位数字之和 |
| 10 | string num = "12345"; |
| 11 | int sum = 0; |
| 12 | for (char c : num) |
| 13 | { |
| 14 | sum += c - '0'; |
| 15 | } |
| 16 | cout << 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 |
一个字符串的“一部分”叫子串(比如 "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'。
s.find("World") 返回 6("World" 的第一个字符 W 的下标)。再用 s.substr(6, 5):从下标 6 开始,截取 5 个字符,就得到高亮的 "World"。空格也是一个字符(图中用 ␣ 表示),所以 H 到 o 加空格正好占了下标 0~5。| 1 | string s = "Hello World, Hello C++"; |
| 2 | |
| 3 | size_t pos = s.find("Hello"); // 从左往右找,返回首次出现的下标 |
| 4 | cout << pos << endl; // 输出:0 |
| 5 | |
| 6 | pos = s.find("Hello", 1); // 从下标 1 开始往后找,跳过开头那个 Hello |
| 7 | cout << pos << endl; // 输出:13 |
| 8 | |
| 9 | pos = s.find('W'); // 也可以找单个字符 |
| 10 | cout << pos << endl; // 输出:6 |
| 11 | |
| 12 | pos = s.find("xyz"); // 找不到,返回 string::npos |
| 13 | if (pos == string::npos) |
| 14 | { |
| 15 | cout << "没找到 xyz" << endl; |
| 16 | } |
| 17 | |
| 18 | pos = s.rfind("o"); // 从右往左找,返回最后一次出现的下标 |
| 19 | cout << pos << endl; // 输出:17 |
| 20 | |
| 21 | pos = s.find_first_of("aeiouAEIOU"); // 找任意一个元音字母第一次出现的位置 |
| 22 | cout << pos << "(字符 " << s[pos] << ")" << endl; // 输出:1(字符 e) |
🔁 综合:找出所有出现的位置
想找“所有”出现的位置,就用 while 循环:每找到一个,就从它的下一个位置继续往后找,直到返回 npos 为止。
| 1 | string s = "Hello World, Hello C++"; |
| 2 | |
| 3 | cout << "所有 l 的位置:"; |
| 4 | size_t pos = s.find('l'); |
| 5 | while (pos != string::npos) |
| 6 | { |
| 7 | cout << pos << " "; |
| 8 | pos = s.find('l', pos + 1); // 从刚找到的位置的下一个位置继续找 |
| 9 | } |
| 10 | cout << 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(起始位置, 截取长度) 从原字符串中“剪”出一段,生成一个新的字符串,原字符串保持不变。如果省略长度,就一直截到末尾。
| 1 | string s = "Hello World"; |
| 2 | |
| 3 | string sub = s.substr(6, 5); // 从下标 6 开始,截取 5 个字符 |
| 4 | cout << sub << endl; // 输出:World |
| 5 | |
| 6 | cout << s.substr(6) << endl; // 省略长度,截到末尾,输出:World |
| 7 | cout << s.substr(0, 5) << endl; // 前 5 个字符,输出:Hello |
| 8 | cout << s.substr(s.size() - 5) << endl; // 后 5 个字符,输出:World |
| 9 | cout << s << endl; // 原串没变,输出:Hello World |
s.substr(0, n) 取前 n 个字符,s.substr(s.size() - n) 取后 n 个字符。如果长度写得比剩下的字符还多,会自动截到末尾,不会报错;但起始位置比字符串长度还大就会抛出异常。🧩 实战:find + substr 拆分字符串
下面两个例子都是“先 find 找分隔符,再 substr 截取”的经典用法:提取邮箱的用户名和域名,以及把用逗号隔开的内容拆开。
| 1 | // 例一:提取邮箱的用户名和域名 |
| 2 | string email = "student@school.edu.cn"; |
| 3 | size_t atPos = email.find('@'); // '@' 的位置是 7 |
| 4 | string username = email.substr(0, atPos); // 前面部分:student |
| 5 | string domain = email.substr(atPos + 1); // 后面部分:school.edu.cn(+1 是为了跳过 '@' 本身) |
| 6 | cout << "用户名:" << username << ",域名:" << domain << endl; |
| 7 | |
| 8 | // 例二:拆分逗号分隔的字符串 |
| 9 | string csv = "apple,banana,orange"; |
| 10 | size_t p1 = csv.find(','); // 第一个逗号,下标 5 |
| 11 | size_t p2 = csv.find(',', p1 + 1); // 第二个逗号,下标 12 |
| 12 | cout << csv.substr(0, p1) << endl; // 输出:apple |
| 13 | cout << csv.substr(p1 + 1, p2 - p1 - 1) << endl; // 输出:banana |
| 14 | cout << csv.substr(p2 + 1) << endl; // 输出:orange |
banana 从 p1 + 1 开始,到 p2(第二个逗号)前面结束,所以长度是 p2 - p1 - 1。可以自己验算:12 - 5 - 1 = 6,banana 正好 6 个字母。📝 插入、删除与替换:insert、erase、replace
这三个操作都会直接修改原字符串,修改后字符串的长度会自动变长或变短。先看一张图,了解它们各自做了什么:
insert(pos, ...) 是插在下标 pos 的字符前面,原来 pos 及之后的字符都往后挪。insert:插入。s.insert(位置, 内容) 在指定位置之前插入内容;s.insert(位置, n, 字符) 一次插入 n 个相同的字符。
| 1 | string s1 = "Hello World"; |
| 2 | s1.insert(6, "Beautiful "); // 在下标 6 之前插入 |
| 3 | cout << s1 << endl; // 输出:Hello Beautiful World |
| 4 | |
| 5 | string s2 = "World"; |
| 6 | s2.insert(0, "Hello "); // 在最开头插入 |
| 7 | cout << s2 << endl; // 输出:Hello World |
| 8 | |
| 9 | string s3 = "ABC"; |
| 10 | s3.insert(1, 3, '-'); // 在下标 1 之前插入 3 个 '-' |
| 11 | cout << s3 << endl; // 输出:A---BC |
| 12 | |
| 13 | // 应用:把手机号格式化成 138-1234-5678 |
| 14 | string phone = "13812345678"; |
| 15 | phone.insert(3, "-"); // 138-12345678 |
| 16 | phone.insert(8, "-"); // 138-1234-5678(第一次插入后,后面的字符整体后移了一位) |
| 17 | cout << phone << endl; |
erase:删除。s.erase(起始位置, 删除长度) 删掉指定范围的字符;省略长度就删到末尾;删单个字符就写 s.erase(pos, 1)。
| 1 | string s4 = "ABCDEFG"; |
| 2 | s4.erase(2, 3); // 从下标 2 开始,删掉 3 个字符(C D E) |
| 3 | cout << s4 << endl; // 输出:ABFG |
| 4 | |
| 5 | string s5 = "Hello World"; |
| 6 | s5.erase(5); // 省略长度:从下标 5 一直删到末尾 |
| 7 | cout << s5 << endl; // 输出:Hello |
| 8 | |
| 9 | // 结合 find:删掉指定内容 |
| 10 | string s6 = "Hello World"; |
| 11 | size_t pos = s6.find("Wo"); |
| 12 | if (pos != string::npos) |
| 13 | { |
| 14 | s6.erase(pos, 2); // 删掉 "Wo" |
| 15 | } |
| 16 | cout << s6 << endl; // 输出:Hello rld |
| 17 | |
| 18 | // 循环 find + erase:删掉全部空格 |
| 19 | string s7 = "a b c d e"; |
| 20 | pos = s7.find(' '); |
| 21 | while (pos != string::npos) |
| 22 | { |
| 23 | s7.erase(pos, 1); |
| 24 | pos = s7.find(' ', pos); // 注意:不用 +1,因为删掉空格后,后面的字符已经往前补位了 |
| 25 | } |
| 26 | cout << s7 << endl; // 输出:abcde |
replace:替换。s.replace(起始位置, 被替换的长度, 新内容) 把指定范围的字符换成新内容,新内容和被换掉的部分长度可以不一样,字符串总长度会自动调整。新内容写空字符串 "",效果就等于 erase。
| 1 | string s8 = "I like Java"; |
| 2 | s8.replace(7, 4, "C++"); // 把下标 7 起的 4 个字符 "Java" 换成 "C++" |
| 3 | cout << s8 << endl; // 输出:I like C++ |
| 4 | |
| 5 | string s9 = "ABCDEFG"; |
| 6 | s9.replace(2, 3, "XYZ"); // 把 "CDE" 换成 "XYZ" |
| 7 | cout << s9 << endl; // 输出:ABXYZFG |
| 8 | |
| 9 | string s10 = "ABxxxCD"; |
| 10 | s10.replace(2, 3, ""); // 替换成空 = 删除 |
| 11 | cout << s10 << endl; // 输出:ABCD |
| 12 | |
| 13 | // 循环 find + replace:把所有 "one" 换成 "1" |
| 14 | string s11 = "one two one two"; |
| 15 | size_t pos = s11.find("one"); |
| 16 | while (pos != string::npos) |
| 17 | { |
| 18 | s11.replace(pos, 3, "1"); |
| 19 | pos = s11.find("one", pos + 1); |
| 20 | } |
| 21 | cout << s11 << endl; // 输出:1 two 1 two |
"1" 里不包含要找的 "one",所以 pos + 1 没问题。但如果新内容里又包含了要找的内容——比如把 "a" 替换成 "aa"——每替换一次就会产生一个新的 "a",用 pos + 1 会一直找到刚替换出来的,永远停不下来。pos 直接跳过新内容:pos += 新内容的长度;(上面的例子里新内容长度是 1,所以 pos + 1 恰好可用)。insert、erase、replace 都直接修改原字符串。如果之后还需要原来的内容,请先复制一份:string backup = s;。out_of_range 异常。🧪 综合实战:五个操作一起用
任务:处理字符串 " Hello World! "(注意两头各有一个空格)。依次完成:① 查找 World 的位置;② 截取出 World!;③ 在 World 后面插入 " C++";④ 删除所有空格;⑤ 把 Hello 替换成 Hi。
| 1 | #include <iostream> |
| 2 | #include <string> |
| 3 | using namespace std; |
| 4 | |
| 5 | int 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 开始。字符串可以看成“一排字符”,所以 <algorithm> 里处理序列的工具,也能直接用在 string 上。最常用的两个是 sort(排序)和 reverse(反转)。它们都直接修改原字符串。
🔀 sort:给字符排队
sort(s.begin(), s.end()) 把字符串里的字符按 ASCII 码从小到大排好。想从大到小,在后面加一个 greater<char>();或者用反向迭代器 sort(s.rbegin(), s.rend())。
| 1 | #include <algorithm> // sort 和 reverse 都需要这个头文件 |
| 2 | #include <functional> // greater 需要这个头文件 |
| 3 | |
| 4 | string s1 = "dcba"; |
| 5 | sort(s1.begin(), s1.end()); // 升序 |
| 6 | cout << s1 << endl; // 输出:abcd |
| 7 | |
| 8 | string s2 = "hello"; |
| 9 | sort(s2.begin(), s2.end(), greater<char>()); // 降序 |
| 10 | cout << s2 << endl; // 输出:ollhe |
| 11 | |
| 12 | // 大小写混合:大写字母的 ASCII 比小写小,所以大写排在前面 |
| 13 | string s3 = "CbAd"; |
| 14 | sort(s3.begin(), s3.end()); |
| 15 | cout << s3 << endl; // 输出:ACbd |
string backup = s;。排序规则就是 11.1.3 讲过的 ASCII 大小规律:数字 < 大写字母 < 小写字母。↩️ reverse:把字符串倒过来
reverse(s.begin(), s.end()) 把整个字符串首尾颠倒。只想倒前面一部分,就把结束位置改成 s.begin() + n,只会反转前 n 个字符。
| 1 | string s4 = "ABCDEFG"; |
| 2 | reverse(s4.begin(), s4.end()); |
| 3 | cout << s4 << endl; // 输出:GFEDCBA |
| 4 | |
| 5 | string s5 = "ABCDEFG"; |
| 6 | reverse(s5.begin(), s5.begin() + 3); // 只反转前 3 个字符 |
| 7 | cout << s5 << endl; // 输出:CBADEFG |
🪞 应用:判断回文串
回文串就是正着读和倒着读都一样的字符串,比如 "level"、"racecar"。判断方法很直接:把字符串复制一份,反转之后和原来的比一比,相等就是回文。
| 1 | string s = "level"; |
| 2 | string rev = s; // 先复制一份 |
| 3 | reverse(rev.begin(), rev.end()); // 把复制品反转 |
| 4 | cout << s << (s == rev ? " 是" : " 不是") << "回文串" << endl; // 输出:level 是回文串 |
👉👈 手动反转:双指针法(理解原理)
reverse 的原理其实很简单,就是首尾对称交换:一个指针 left 从最左边出发,一个指针 right 从最右边出发,交换它们指向的字符,然后各自向中间靠拢,直到两个指针碰头为止。这叫“双指针”思想,后面的算法篇还会反复用到。
n 的字符串只需要交换 n/2 次,时间复杂度是 O(n)。因为是在原字符串上“原地”交换,不需要额外开辟新字符串,所以空间复杂度是 O(1)。| 1 | string s = "12345"; |
| 2 | int left = 0, right = (int)s.size() - 1; // 左指针指向开头,右指针指向末尾 |
| 3 | while (left < right) |
| 4 | { |
| 5 | swap(s[left], s[right]); // 交换两个指针指向的字符 |
| 6 | left++; // 左指针向右移 |
| 7 | right--; // 右指针向左移 |
| 8 | } |
| 9 | cout << s << endl; // 输出:54321 |
| 函数 | 功能 | 是否修改原串 | 需要头文件 |
|---|---|---|---|
sort(s.begin(), s.end()) | 升序排列字符 | ✅ 原地修改 | <algorithm> |
reverse(s.begin(), s.end()) | 颠倒字符顺序 | ✅ 原地修改 | <algorithm> |
把 string 想象成一排连续的格子,每个格子里放一个字符。迭代器就像夹在格子之间的"书签"——它记住了你当前所在的位置。你可以用 *it 读取当前格子的字符,用 ++it 把书签往后移一格。(迭代器的概念在 11.0 里介绍过,11.1.5 里你也已经见过它的写法,这一节再把含义讲清楚。)
| 1 | string s = "Hi"; |
| 2 | // begin() 指向第一个字符,end() 指向尾后位置 |
| 3 | for (auto it = s.begin(); it != s.end(); ++it) |
| 4 | { |
| 5 | cout << *it << " "; // 输出:H i |
| 6 | } |
| 7 | |
| 8 | // C++11 范围 for 循环(底层也是用迭代器) |
| 9 | for (char c : s) |
| 10 | { |
| 11 | cout << c; // 输出:Hi |
| 12 | } |
s.begin() 返回指向第一个字符的迭代器,s.end() 返回指向最后一个字符后面的迭代器——它不指向任何字符,只表示"已经遍历完了"。前面 sort、reverse 里写的 s.begin()、s.end(),就是在告诉函数“处理从头到尾这一段”。把本节出现过的方法按用途归类汇总,写代码时可以直接当参考卡用。
| 方法 | 分类 | 作用 |
|---|---|---|
| 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) | 其它 | 交换两个字符串的全部内容 |
string 是 C++ 处理字符串的核心类型,比 char 数组更安全、更方便;用 = 赋值,用 .size() / .length() 获取长度。+、+= 拼接,用 ==、<、> 按字典序比较。注意两个双引号字符串不能直接相加,size() 是无符号数。cin 读不含空格的内容,getline() 读整行;混用时要先清掉残留的换行符(cin.ignore(),或者多读一次 getline)。[i] 或 .at(i) 访问、修改字符,下标从 0 开始,用范围 for 或下标循环遍历。stoi / stoll / stod,数字 → 字符串用 to_string,数字字符和数字之间用 - '0' / + '0'。find 找位置、substr 截取、insert 插入、erase 删除、replace 替换,常常用 find 定位后配合使用。sort 给字符排序,reverse 反转字符顺序,两者都直接修改原字符串,需要 <algorithm>。