第 20 章 字符串的十八般武艺
🏗️ 前情回顾:上一章你学会了
string的基本功——声明、输入、拼接、求长度、数字转换。文字能存了、能读了、能拼了。但你想过吗——存进去的文字,能像编辑 Word 文档一样随意修改吗?能查找某个词在第几个字的位置吗?能截取第一句话吗?能删除多余的空格吗?能统计文章里有多少个数字吗?💡 本章要解决的问题:string 能像编辑文档一样——查找、截取、删除、插入、替换吗? 这一章,我们解锁
string的全部"编辑功能",让你的代码像一个微型文字处理器。
🎯 本章目标
学完这一章,你能:
- 用
find在字符串中定位某个子串的位置 - 用
substr从字符串中截取一段 - 用
erase、insert、replace修改字符串 - 用
isdigit、isalpha、toupper、tolower处理单个字符 - 用下标遍历和范围 for 两种方式遍历字符串
📖 故事引入
小杰在编辑一篇作文。他发现:
- 文章里有个错别字重复出现了好多次,他想全部替换掉。
- 他想把文章的第一段单独复制出来。
- 他想删掉某个多余的句子。
- 他想统计文章里有多少个数字。
如果在纸上做这些事,小杰得拿剪刀、胶水、修正带、荧光笔……忙活一下午。但如果作文存在 string 里,这些操作全都可以用一行代码搞定。
string 就像一个自带编辑功能的文字容器——查找、剪切、粘贴、替换,信手拈来。这一章,我们就来掌握这套"文字编辑功夫"。
🧱 知识讲解
20.1 find:大海捞针
find 在一大段文字里找一小段文字(子串),返回第一次出现的位置(下标)。如果找不到,返回一个特殊值 string::npos。
string s = "I love programming, love C++";
int pos = s.find("love"); // pos = 2(第一个 love 在位置 2)
cout << pos << endl; // 2
pos = s.find("love", 5); // 从下标 5 开始找
cout << pos << endl; // 18(第二个 love 在位置 18)
pos = s.find("Python");
if (pos == string::npos) {
cout << "没找到" << endl; // 输出这个
}❌ 常见踩坑——find 返回值判断用 -1:
cppif (s.find("abc") == -1) { ... } // ❌ 有些编译器能用,但不规范✅ 正确——标准写法:
cppif (s.find("abc") == string::npos) { ... } // ✅ C++ 标准写法🧠
string::npos是一个特殊常量,表示"没有这个位置"(no position)。它的值通常是 -1,但直接用string::npos最规范。
实战:统计某个词出现了几次:
string s = "apple banana apple orange apple";
string target = "apple";
int cnt = 0;
int pos = 0;
while ((pos = s.find(target, pos)) != string::npos) {
cnt++;
pos += target.size(); // 跳过已找到的,继续找下一个
}
cout << "apple 出现了 " << cnt << " 次" << endl; // 320.2 substr:剪刀裁剪
substr 像一把剪刀,从字符串中间剪下一段。
string s = "Hello World";
string sub1 = s.substr(0, 5); // 从下标 0 开始,取 5 个 → "Hello"
string sub2 = s.substr(6, 5); // 从下标 6 开始,取 5 个 → "World"
string sub3 = s.substr(6); // 从下标 6 开始,取到末尾 → "World"格式:s.substr(起始位置, 长度)。不写长度就取到末尾。
❌ 常见踩坑——substr 取的长度超出字符串长度:
cppstring s = "Hi"; string sub = s.substr(0, 10); // ❌ 长度 10 超过字符串,行为不确定✅ 正确:先判断长度够不够:
cppif (s.size() >= 10) { string sub = s.substr(0, 10); }
实战:判断一个字符串是不是另一个的前缀:
string s = "hello.cpp";
if (s.substr(0, 5) == "hello") {
cout << "这是 C++ 源文件" << endl;
}20.3 erase:橡皮擦
erase 从字符串中删除一段。
string s = "Hello Beautiful World";
s.erase(5, 10); // 从下标 5 开始,删 10 个字符
cout << s << endl; // "Hello World"
s.erase(5); // 从下标 5 开始,删到末尾
cout << s << endl; // "Hello"
s.erase(); // 全部删光
cout << s << endl; // ""实战:去掉字符串的首尾空格(简化版——只去掉开头空格):
string s = " Hello";
while (s.size() > 0 && s[0] == ' ') {
s.erase(0, 1); // 删掉第 0 个字符(每次删 1 个)
}
cout << "[" << s << "]" << endl; // [Hello]20.4 insert:插入
insert 在指定位置塞入一段新文字。
string s = "Hello World";
s.insert(5, " Beautiful"); // 在下标 5 的位置插入
cout << s << endl; // "Hello Beautiful World"
s.insert(0, "Say: "); // 在开头插入
cout << s << endl; // "Say: Hello Beautiful World"20.5 replace:替换
replace 把一段旧的换成一段新的。
string s = "I like cats";
s.replace(2, 4, "love"); // 从下标 2 开始,删 4 个字符,放入 "love"
cout << s << endl; // "I love cats"
s.replace(7, 4, "dogs"); // 把 "cats" 换成 "dogs"
cout << s << endl; // "I love dogs"格式:s.replace(起始位置, 要删的长度, 新字符串)——先删后插。
20.6 字符处理函数
这些函数处理单个字符(char),不是整个 string。需要 #include <cctype>。
| 函数 | 作用 | 示例 |
|---|---|---|
isdigit(c) | c 是数字吗? | isdigit('5') → true |
isalpha(c) | c 是字母吗? | isalpha('A') → true |
isupper(c) | c 是大写吗? | isupper('Z') → true |
islower(c) | c 是小写吗? | islower('a') → true |
isspace(c) | c 是空白吗? | isspace(' ') → true |
toupper(c) | 转成大写 | toupper('a') → 'A' |
tolower(c) | 转成小写 | tolower('Z') → 'z' |
❌ 常见踩坑——toupper/tolower 没赋值回去:
cppstring s = "hello"; for (int i = 0; i < s.size(); i++) { toupper(s[i]); // ❌ 返回了大写,但没存回 s[i]! } cout << s << endl; // 还是 "hello",白干了✅ 正确——必须把返回值存回去:
cppfor (int i = 0; i < s.size(); i++) { s[i] = toupper(s[i]); // ✅ 把转换结果赋值回去 } cout << s << endl; // "HELLO"🧠
toupper(c)不修改 c 本身,而是返回转换后的新值。不用返回值 = 白调。
实战:统计字符串中的数字个数:
string s = "a1b2c3d4";
int cnt = 0;
for (int i = 0; i < s.size(); i++) {
if (isdigit(s[i])) cnt++;
}
cout << "数字个数:" << cnt << endl; // 4实战:大小写转换:
string s = "Hello World";
for (int i = 0; i < s.size(); i++) {
s[i] = toupper(s[i]); // 全部转大写
}
cout << s << endl; // "HELLO WORLD"⚠️ 别忘了
#include <cctype>!用了isdigit/toupper但没 include → 编译可能不报错但行为怪异。
20.7 遍历字符串的两种方式
方式一:下标遍历——你已经很熟悉了:
string s = "Hello";
for (int i = 0; i < s.size(); i++) {
cout << s[i] << " ";
}
// H e l l o方式二:范围 for 循环——C++11 的新语法,更简洁:
string s = "Hello";
for (char c : s) { // 读作:对于 s 里的每个字符 c
cout << c << " ";
}
// H e l l o范围 for 的"读法口诀":
for (元素类型 变量名 : 容器)→ "对于容器里的每个元素"。
如果需要修改每个字符,用引用:
for (char &c : s) { // 注意 & ——引用,可以修改
c = toupper(c);
}
cout << s << endl; // "HELLO"两种方式各有优劣:下标遍历可以知道位置(i),范围 for 写法更简洁。按需选择。
✋ 动手试试
试试 1:写一个字符串 "apple orange apple banana apple",用 find 循环统计 "apple" 出现了多少次。
试试 2:用户输入一个字符串,用 substr 分别取出前 3 个字符和后 3 个字符。如果长度不够 3,输出"太短了"。
试试 3:用户输入一段英文,用 toupper 把每个单词的首字母变成大写(提示:空格后的第一个字母是单词首字母)。
🦶 你踩过这些坑吗?
- [ ] 坑 1:
find返回值用== -1判断找不到——该用== string::npos - [ ] 坑 2:
substr取的长度超出字符串长度——忘了先判断长度 - [ ] 坑 3:写了
toupper(s[i])但没赋值回s[i]——白调了,原字符没变 - [ ] 坑 4:用了
isdigit/toupper但没#include <cctype>——可能编译通过但行为出错 - [ ] 坑 5:用范围 for 遍历想修改字符但没加
&——char c是拷贝,改了也不影响原字符串
📝 练习
基础题
1. 选择题
(1)s.find("abc") 没找到时返回什么?
A. -1 B. 0 C. string::npos D. s.size()
(2)s.substr(2, 3) 的意思是:
A. 从位置 2 开始,取到末尾 B. 从位置 2 开始,取 3 个字符 C. 取第 2 和第 3 个字符 D. 截掉前 2 个字符,保留 3 个
(3)判断字符 c 是否为字母的函数是:
A. isdigit(c) B. isalpha(c) C. isletter(c) D. ischar(c)
(4)toupper('b') 的返回值是:
A. 'b' B. 'B' C. 98 D. 不改变原字符
2. 填空题
(1)把字符串 s 中从下标 3 开始、长 5 的一段替换为 "123":s.replace(____, ____, "____");
(2)范围 for 遍历字符串的语法:for (____变量名 :____)
(3)erase、insert、replace 三个函数中的位置参数都是 ____(下标/序号)。
提高题
3. 编程题 — 替换敏感词
用户输入一句话,程序把其中的 "傻瓜" 全部替换成 "**"。(提示:用 find 循环定位 + replace 替换。)
4. 编程题 — 统计字符类型
用户输入一个字符串,统计其中大写字母、小写字母、数字、其他字符各有多少个。
5. 编程题 — 隐藏手机号
用户输入一个 11 位手机号字符串,程序将中间 4 位替换为 **** 后输出。例如输入 13812345678,输出 138****5678。(提示:用 substr 取头尾 + 拼接,或用 replace 直接替换中间部分。)
挑战题
6. 编程题 — 判断回文串
回文串就是正着读和反着读一样的字符串(如 "racecar"、"level"、"上海自来水来自海上")。用户输入一个字符串,判断它是不是回文串。忽略大小写和非字母数字字符。
示例:输入 "A man, a plan, a canal: Panama" → 输出 是回文串
(提示:先预处理——用 tolower 统一小写,用 isalnum 过滤标点,然后双指针从两端向中间比较。)
7. 编程题 — 压缩字符串
实现简单行程编码(Run-Length Encoding):对于连续重复的字符,用"字符+次数"表示。例如 "aaabbc" 压缩为 "a3b2c1","abc" 压缩为 "a1b1c1"。输出压缩后的字符串。
🧠 本章小结
查找 find(subs, start=0) → 返回下标或 string::npos
截取 substr(pos, len) → 返回截取的子串
删除 erase(pos, len) → 删除指定段
插入 insert(pos, str) → 在 pos 处插入
替换 replace(pos, len, str) → 删 len 个,再插入 str
字符处理(需 <cctype>):
isdigit/isalpha/isupper/islower/isspace
toupper/tolower(返回转换后值,要赋值回去!)
遍历方式:
下标 for (int i=0; i<s.size(); i++)
范围 for (char c : s) / for (char &c : s)📝 配套练习
共9题。string操作密集——find/substr/erase/insert四个方法每道题练2-3个。。★核心(课堂必做) ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | J0091 | https://hydro.ac/d/srqc/p/J0091 | find、循环查找、位置更新 |
| ◆ 拓展 | J0090 | https://hydro.ac/d/srqc/p/J0090 | substr/erase/insert三连 |
| ◆ 拓展 | J0095 | https://hydro.ac/d/srqc/p/J0095 | find、substr、符号处理 |
| ◆ 拓展 | J0096 | https://hydro.ac/d/srqc/p/J0096 | string、字符映射解码 |
| ◆ 拓展 | J0087 | https://hydro.ac/d/srqc/p/J0087 | string、桶计数、质数判定 |
| ◆ 拓展 | luogu-P5734 | https://hydro.ac/p/luogu-P5734 | find、substr、insert、erase全练 |
| ◆ 拓展 | J0094 | https://hydro.ac/d/srqc/p/J0094 | find、字符替换、标记数组 |
| ◆ 拓展 | J0099 | https://hydro.ac/d/srqc/p/J0099 | string、桶统计、柱状图输出 |
| ◆ 拓展 | J0098 | https://hydro.ac/d/srqc/p/J0098 | string、字符解析、赋值 |
💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 8 道◆拓展题,覆盖不同变式和细节。
配套练习
共9题。string操作密集——find/substr/erase/insert四个方法每道题练2-3个。★核心(课堂必做) · ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | J0091 | https://hydro.ac/d/srqc/p/J0091 | find、循环查找、位置更新 |
| ◆ 拓展 | J0090 | https://hydro.ac/d/srqc/p/J0090 | substr/erase/insert三连 |
| ◆ 拓展 | J0095 | https://hydro.ac/d/srqc/p/J0095 | find、substr、符号处理 |
| ◆ 拓展 | J0096 | https://hydro.ac/d/srqc/p/J0096 | string、字符映射解码 |
| ◆ 拓展 | J0087 | https://hydro.ac/d/srqc/p/J0087 | string、桶计数、质数判定 |
| ◆ 拓展 | luogu-P5734 | https://hydro.ac/p/luogu-P5734 | find、substr、insert、erase全练 |
| ◆ 拓展 | J0094 | https://hydro.ac/d/srqc/p/J0094 | find、字符替换、标记数组 |
| ◆ 拓展 | J0099 | https://hydro.ac/d/srqc/p/J0099 | string、桶统计、柱状图输出 |
| ◆ 拓展 | J0098 | https://hydro.ac/d/srqc/p/J0098 | string、字符解析、赋值 |
练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 8 道◆拓展题,覆盖不同变式和细节。
自查清单:
- [ ] 我会用
find查找子串并用string::npos判断找不到 - [ ] 我会用
substr截取字符串的任意一段 - [ ] 我会用
erase、insert、replace修改字符串 - [ ] 我会用字符处理函数做判断和转换(且知道
toupper要赋值回去) - [ ] 我会用下标和范围 for 两种方式遍历字符串
- [ ] 我知道何时用
char c、何时用char &c(想修改原字符串时用引用) - [ ] 我能把
find+replace组合起来实现"批量替换"
🚀 下章预告
字符串的十八般武艺练完了——find、substr、erase、toupper……等等,toupper('a') 怎么就知道要返回 'A'?字符 'a' 和数字 97 之间到底是什么关系?为什么 'A' + 1 会变成 'B'?
这些问题的答案藏在计算机最底层的"翻译规则"里——每个字符背后都有一个数字编号。下一章,我们走进 ASCII 的世界,揭开字符与编码的秘密:为什么字母能当数字用?0 和 1 怎么变成文字?间谍怎么用数字传密信?