C++ practical course · lesson 12

C++ 字符串与文本解析

命令行程序接收到的很多信息都是文字。本课学习怎样逐字符检查、切分单词、查找内容并把一整行命令解析成程序能处理的数据。

Easy explanation

字符串像一列有编号的字符车厢。

一句话在我们眼中有含义,在程序眼中却是一串按顺序排列的字符。每个字符有位置,可以读取、比较、替换或删除。理解这一点后,“统计数字个数”就变成逐个检查字符,“找出用户名”就变成寻找分隔符再截取一段。

文本解析最重要的不是某个函数名,而是先约定格式。例如 add 数学作业 3 到底有几个字段?任务名能不能含空格?缺少数字怎么办?格式约定越明确,解析越可靠。面对不可信的用户文字,程序要验证长度、字符范围和字段数量,不能假设输入永远完美。

目标 1安全访问、遍历和修改 string
目标 2使用 find 与 substr 提取文本
目标 3用 stringstream 完成分词与转换
目标 4设计并验证简单命令格式
C++ 字符串与文本解析通俗学习插图
从字符、单词到完整命令,文本处理也是逐层拆分问题。

Core concepts

先把四个核心知识点说清楚。

01

长度与下标

text.size() / text.at(i)

下标从零开始。at 越界时会报告错误,适合初学阶段发现位置问题。

02

查找位置

size_t pos = text.find(":");

找不到时返回 string::npos,使用位置前必须检查,不能把 npos 当普通下标。

03

截取片段

text.substr(start, length)

从 start 开始取指定长度。先确认起点合法,并分清第二个参数是长度而不是结束位置。

04

流式分词

stringstream ss(line); ss >> word;

字符串流像 cin 一样从一行文字中读取单词和数字,适合空格分隔的简单格式。

for (char ch : text)

范围循环适合逐字符统计,不需要手动管理下标。

if (isdigit(static_cast<unsigned char>(ch)))

字符分类函数前转换为 unsigned char 更稳妥。

transform(text.begin(), text.end(), text.begin(), ::tolower);

统一大小写后再比较,搜索更符合用户预期。

getline(ss, field, ","[0]);

指定分隔符可以逐项读取简单 CSV,但仍要考虑引号等复杂情况。

Complete examples

两个例子,把知识变成能运行的程序。

Example 1

例子一:句子分析器

第一遍逐字符统计字母、数字和空格;第二遍用 stringstream 读取单词并寻找最长单词。两个任务分开后逻辑更清楚。

最长单词初始为空,每遇到更长单词就替换。标点会附在单词上,进阶版本可以先清理标点。

逐字符统计与逐词读取
string line;
getline(cin, line);
int letters = 0, digits = 0, spaces = 0;
for (char ch : line)
{
    unsigned char safe = static_cast<unsigned char>(ch);
    if (isalpha(safe)) ++letters;
    else if (isdigit(safe)) ++digits;
    else if (isspace(safe)) ++spaces;
}

stringstream ss(line);
string word, longest;
int words = 0;
while (ss >> word)
{
    ++words;
    if (word.size() > longest.size()) longest = word;
}
cout << letters << ' ' << digits << ' ' << words << ' ' << longest;
输入:C++ lesson 12 is useful字母:15,数字:2单词:5最长单词:lesson

Example 2

例子二:解析任务命令

约定命令格式为 add 名称|优先级,竖线让名称可以包含空格。程序先分离动作,再寻找竖线并转换数字。

每一步都可能失败,因此先检查空格、分隔符和数字范围。解析成功后再创建任务,避免半成品数据进入列表。

读取动作、名称和优先级
string line = "add 数学作业|3";
size_t firstSpace = line.find(' ');
if (firstSpace == string::npos) return 1;

string action = line.substr(0, firstSpace);
string rest = line.substr(firstSpace + 1);
size_t bar = rest.rfind('|');
if (bar == string::npos) return 1;

string name = rest.substr(0, bar);
int priority = stoi(rest.substr(bar + 1));
if (action == "add" && !name.empty() && priority >= 1 && priority <= 5)
    cout << "已添加:" << name << ",优先级 " << priority;
命令:add 数学作业|3动作:add名称:数学作业优先级:3

Algorithm thinking

解析文本要从格式最外层逐步向内。

不要同时寻找所有字段。先确认整行是否为空,再分离最稳定的外层结构,然后逐个验证内部字段。每一步只接受已经满足前一步条件的文本,错误信息也能准确指出缺少什么。

文本算法经常涉及位置和长度。画出字符下标,手算 find 返回的位置以及 substr 取出的范围。对空字符串、没有分隔符、分隔符在开头或结尾、连续多个空格都要测试。

  1. 1
    定义格式

    明确字段顺序、分隔符、是否允许空格和缺省值。

  2. 2
    找到边界

    用 find 定位分隔符,找不到时立即返回清楚错误。

  3. 3
    提取并清理

    用 substr 取字段,去掉多余空格并统一大小写。

  4. 4
    转换和验证

    数字转换可能失败,字段长度与取值范围也要检查。

Common mistakes

这些错误很常见,学会自己排查。

1

访问越界

循环条件写成 i <= text.size() 会多访问一次,应使用小于。

2

忽略 npos

find 失败后继续 substr,会得到异常或错误结果。

3

混淆位置与长度

substr 的第二个参数是字符数量,不是结束下标。

4

stoi 没有保护

用户输入 abc 时转换会抛异常,调用前检查格式或捕获异常。

本课通用调试法

先准备一个最小输入,只保留能够重现问题的几行数据;再在关键步骤输出变量值,确认程序究竟在哪一步偏离预期。编译错误从第一条开始处理,运行错误则比较“实际结果”和“期望结果”。修复后别只重跑原来的例子,还要增加空输入、边界值和错误输入,防止问题换一个形式再次出现。

Homework

第 12 课课后作业:从模仿到独立完成。

建议按顺序完成。前四题帮助巩固语法和基本操作,第五到第七题要求把多个知识点组合起来,第八题是小项目。每道题都要先写输入、处理、输出三行计划,再开始敲代码;程序运行后至少测试正常情况、边界情况和错误情况。

1

字符统计

统计一行中的大写、小写、数字、空格和其他字符,并验证空行。

2

回文判断

忽略大小写和空格判断一句话是否回文,先生成清理后的字符串再比较。

3

邮箱检查

检查是否恰好有一个 @、两边非空且后半部分包含点,输出具体失败原因。

4

单词替换

把句子中所有完整目标单词替换为新单词,不能误改较长单词的一部分。

5

CSV 拆分

解析姓名、年龄、城市三列,检查列数和年龄格式,再输出表格。

6

词频统计

统一大小写并清理标点,统计每个单词出现次数,为第 16 课映射做准备。

7

迷你命令解释器

支持 add、remove、show 和 quit,验证每种命令需要的参数数量。

8

日志分析项目

读取多行 LEVEL|TIME|MESSAGE 格式日志,统计级别并查找包含关键词的记录。

本课验收标准:能安全处理空字符串和找不到分隔符;不发生下标越界;至少使用 find、substr、getline 和 stringstream;解析失败时能指出具体字段问题。

提交内容应包括源代码、三组测试输入与输出、一个曾经出现的错误及修复方法。最后请用自己的话解释本课最重要的概念;如果只能照着代码念,还需要再独立重写一次核心例子。

Lesson 12 summary

文本解析,就是不断确认边界、提取片段和验证格式。

stringfind / npossubstrstringstream文本格式

下一课会把解析出来的多条记录放进 vector,并完成增加、删除、修改、查找和统计。