Skip to content

第 20 章 字符串的十八般武艺


🏗️ 前情回顾:上一章你学会了 string 的基本功——声明、输入、拼接、求长度、数字转换。文字能存了、能读了、能拼了。但你想过吗——存进去的文字,能像编辑 Word 文档一样随意修改吗?能查找某个词在第几个字的位置吗?能截取第一句话吗?能删除多余的空格吗?能统计文章里有多少个数字吗?

💡 本章要解决的问题string 能像编辑文档一样——查找、截取、删除、插入、替换吗? 这一章,我们解锁 string 的全部"编辑功能",让你的代码像一个微型文字处理器。


🎯 本章目标

学完这一章,你能:

  • find 在字符串中定位某个子串的位置
  • substr 从字符串中截取一段
  • eraseinsertreplace 修改字符串
  • isdigitisalphatouppertolower 处理单个字符
  • 用下标遍历和范围 for 两种方式遍历字符串

📖 故事引入

小杰在编辑一篇作文。他发现:

  • 文章里有个错别字重复出现了好多次,他想全部替换掉。
  • 他想把文章的第一段单独复制出来。
  • 他想删掉某个多余的句子。
  • 他想统计文章里有多少个数字。

如果在纸上做这些事,小杰得拿剪刀、胶水、修正带、荧光笔……忙活一下午。但如果作文存在 string 里,这些操作全都可以用一行代码搞定。

string 就像一个自带编辑功能的文字容器——查找、剪切、粘贴、替换,信手拈来。这一章,我们就来掌握这套"文字编辑功夫"。


🧱 知识讲解

20.1 find:大海捞针

find 在一大段文字里找一小段文字(子串),返回第一次出现的位置(下标)。如果找不到,返回一个特殊值 string::npos

cpp
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:

cpp
if (s.find("abc") == -1) { ... }   // ❌ 有些编译器能用,但不规范

正确——标准写法:

cpp
if (s.find("abc") == string::npos) { ... }  // ✅ C++ 标准写法

🧠 string::npos 是一个特殊常量,表示"没有这个位置"(no position)。它的值通常是 -1,但直接用 string::npos 最规范。

实战:统计某个词出现了几次

cpp
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;  // 3

20.2 substr:剪刀裁剪

substr 像一把剪刀,从字符串中间剪下一段。

cpp
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 取的长度超出字符串长度:

cpp
string s = "Hi";
string sub = s.substr(0, 10);     // ❌ 长度 10 超过字符串,行为不确定

正确:先判断长度够不够:

cpp
if (s.size() >= 10) {
    string sub = s.substr(0, 10);
}

实战:判断一个字符串是不是另一个的前缀

cpp
string s = "hello.cpp";
if (s.substr(0, 5) == "hello") {
    cout << "这是 C++ 源文件" << endl;
}

20.3 erase:橡皮擦

erase 从字符串中删除一段。

cpp
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;                 // ""

实战:去掉字符串的首尾空格(简化版——只去掉开头空格):

cpp
string s = "   Hello";
while (s.size() > 0 && s[0] == ' ') {
    s.erase(0, 1);                 // 删掉第 0 个字符(每次删 1 个)
}
cout << "[" << s << "]" << endl;   // [Hello]

20.4 insert:插入

insert 在指定位置塞入一段新文字。

cpp
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 把一段旧的换成一段新的。

cpp
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 没赋值回去:

cpp
string s = "hello";
for (int i = 0; i < s.size(); i++) {
    toupper(s[i]);                   // ❌ 返回了大写,但没存回 s[i]!
}
cout << s << endl;                   // 还是 "hello",白干了

正确——必须把返回值存回去:

cpp
for (int i = 0; i < s.size(); i++) {
    s[i] = toupper(s[i]);            // ✅ 把转换结果赋值回去
}
cout << s << endl;                   // "HELLO"

🧠 toupper(c) 不修改 c 本身,而是返回转换后的新值。不用返回值 = 白调。

实战:统计字符串中的数字个数

cpp
string s = "a1b2c3d4";
int cnt = 0;
for (int i = 0; i < s.size(); i++) {
    if (isdigit(s[i])) cnt++;
}
cout << "数字个数:" << cnt << endl;  // 4

实战:大小写转换

cpp
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 遍历字符串的两种方式

方式一:下标遍历——你已经很熟悉了:

cpp
string s = "Hello";
for (int i = 0; i < s.size(); i++) {
    cout << s[i] << " ";
}
// H e l l o

方式二:范围 for 循环——C++11 的新语法,更简洁:

cpp
string s = "Hello";
for (char c : s) {                 // 读作:对于 s 里的每个字符 c
    cout << c << " ";
}
// H e l l o

范围 for 的"读法口诀":for (元素类型 变量名 : 容器) → "对于容器里的每个元素"。

如果需要修改每个字符,用引用:

cpp
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 把每个单词的首字母变成大写(提示:空格后的第一个字母是单词首字母)。


🦶 你踩过这些坑吗?

  • [ ] 坑 1find 返回值用 == -1 判断找不到——该用 == string::npos
  • [ ] 坑 2substr 取的长度超出字符串长度——忘了先判断长度
  • [ ] 坑 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)eraseinsertreplace 三个函数中的位置参数都是 ____(下标/序号)。

提高题

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个。。★核心(课堂必做) ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0091https://hydro.ac/d/srqc/p/J0091find、循环查找、位置更新
◆ 拓展J0090https://hydro.ac/d/srqc/p/J0090substr/erase/insert三连
◆ 拓展J0095https://hydro.ac/d/srqc/p/J0095find、substr、符号处理
◆ 拓展J0096https://hydro.ac/d/srqc/p/J0096string、字符映射解码
◆ 拓展J0087https://hydro.ac/d/srqc/p/J0087string、桶计数、质数判定
◆ 拓展luogu-P5734https://hydro.ac/p/luogu-P5734find、substr、insert、erase全练
◆ 拓展J0094https://hydro.ac/d/srqc/p/J0094find、字符替换、标记数组
◆ 拓展J0099https://hydro.ac/d/srqc/p/J0099string、桶统计、柱状图输出
◆ 拓展J0098https://hydro.ac/d/srqc/p/J0098string、字符解析、赋值

💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 8 道◆拓展题,覆盖不同变式和细节。


配套练习

共9题。string操作密集——find/substr/erase/insert四个方法每道题练2-3个。★核心(课堂必做) · ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0091https://hydro.ac/d/srqc/p/J0091find、循环查找、位置更新
◆ 拓展J0090https://hydro.ac/d/srqc/p/J0090substr/erase/insert三连
◆ 拓展J0095https://hydro.ac/d/srqc/p/J0095find、substr、符号处理
◆ 拓展J0096https://hydro.ac/d/srqc/p/J0096string、字符映射解码
◆ 拓展J0087https://hydro.ac/d/srqc/p/J0087string、桶计数、质数判定
◆ 拓展luogu-P5734https://hydro.ac/p/luogu-P5734find、substr、insert、erase全练
◆ 拓展J0094https://hydro.ac/d/srqc/p/J0094find、字符替换、标记数组
◆ 拓展J0099https://hydro.ac/d/srqc/p/J0099string、桶统计、柱状图输出
◆ 拓展J0098https://hydro.ac/d/srqc/p/J0098string、字符解析、赋值

练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 8 道◆拓展题,覆盖不同变式和细节。

自查清单

  • [ ] 我会用 find 查找子串并用 string::npos 判断找不到
  • [ ] 我会用 substr 截取字符串的任意一段
  • [ ] 我会用 eraseinsertreplace 修改字符串
  • [ ] 我会用字符处理函数做判断和转换(且知道 toupper 要赋值回去)
  • [ ] 我会用下标和范围 for 两种方式遍历字符串
  • [ ] 我知道何时用 char c、何时用 char &c(想修改原字符串时用引用)
  • [ ] 我能把 find+replace 组合起来实现"批量替换"

🚀 下章预告

字符串的十八般武艺练完了——findsubstrerasetoupper……等等,toupper('a') 怎么就知道要返回 'A'?字符 'a' 和数字 97 之间到底是什么关系?为什么 'A' + 1 会变成 'B'

这些问题的答案藏在计算机最底层的"翻译规则"里——每个字符背后都有一个数字编号。下一章,我们走进 ASCII 的世界,揭开字符与编码的秘密:为什么字母能当数字用?0 和 1 怎么变成文字?间谍怎么用数字传密信?