[ 问题描述 ]
设计一个简单的行编辑程序,输入一页文字,程序可以统计出文字、数字、空格的个数。静态存储一页文章,每行最多不超过80个字符,共N行。存储结构使用线性表,文字中可以是大写、小写的英文字母、任何数字及标点符号。
[ 基本要求 ]
(1)分别统计出其中英文字母数和空格数及整篇文章总字数;
(2)统计某一字符串在文章中出现的次数,并输出该次数;
(3)删除某一子串,并将后面的字符前移。(4)输出形式:
1)分行输出用户输入的各行字符;
2)分4行输出"全部字母数"、"数字个数"、"空格个数"、"文章总字数"
3)输出删除某一字符串后的文章;
新增功能:
- 单词首字母大写、取消单词首字母大写、
- 统计不同单词个数、删除行 、统计句子个数 以及行数、显示详细字数统计信息、
- 文本排版(左右对齐)、取消文本对齐 、文本居中对齐 、清屏等,
- 实现人机互动界面,自行选择界面等,简便退出程序等。
- 对文本的操作简单,设置了子菜单,明了鲜明。
- 可以从文件中读取数据并且可以将修改后的文件存储到对应的文件中,并且可以选择是覆盖还是追加,进一步更新对应文本文件
需求分析
- 文本操作功能需求:
- 输入输出需求:用户能逐行输入文章内容,也可从文件读取内容,并且程序要能将处理后的文章内容输出显示或保存到文件中,输入时有自动换行处理逻辑,输出时要考虑不同文本对齐状态的展示。
- 统计需求:需要统计文章里字母数、数字数、空格数、总字数、子串出现次数、不同单词个数以及句子个数等,为用户提供全面的文本统计信息。
- 编辑需求:支持删除指定子串和指定行的操作,还能对单词首字母进行大小写转换,方便用户对文本内容进行修改。
- 格式处理需求:提供文本左右对齐、居中对齐以及取消对齐的功能,使文本能以不同的排版格式呈现。
- 交互需求:通过清晰的菜单界面展示所有功能选项,方便用户选择相应操作,并且在操作过程中要有合理的提示信息,引导用户正确输入,如提示输入文件名、选择保存模式等。同时,提供清屏功能优化交互体验(针对 Windows 系统)。
概要设计
- 整体架构设计:采用面向对象编程思想,创建
LineEditor
类来管理文本内容及相关操作,在main
函数中通过循环展示菜单、获取用户选择并调用类中的对应成员函数来实现各种功能交互,整体程序围绕文本的输入、处理、输出这一主线进行设计。 - 类设计:
- 成员变量:
vector<string> lines
:存储文章按行拆分后的文本内容,是后续各种文本操作的基础数据结构。bool isTextJustified
:用于标记文本当前是否处于对齐状态,以此控制文本输出等相关操作的格式逻辑。int lineCount
:记录文章的行数,方便在涉及行操作(如删除行)时进行范围判断等操作。
- 成员函数:设计多个公有成员函数,分别对应不同的文本操作功能,例如
input
负责文本输入,countAllStats
用于统计各种字数,countSub
统计子串出现次数,deleteSub
删除子串等,每个函数承担一项具体的文本处理任务,分工明确,共同构成完整的文本编辑功能集合。
- 成员变量:
- 功能模块划分:
- 输入模块:由
input
函数(从用户终端输入)和readFromFile
函数(从文件读取)组成,在读取文本过程中都按照每行 80 字符的规则进行合理的拆分和存储到lines
中,保证文本数据的有效获取和整理。 - 统计模块:涵盖
countAllStats
、countSub
、countUnique
、countSentences
等函数,从不同维度对文本中的元素进行数量统计,为用户提供多方面的文本统计分析结果。 - 编辑模块:包括
deleteSub
和deleteLine
函数实现文本内容的删除操作,以及capitalizeWords
和uncapitalizeWords
函数处理单词首字母大小写转换,满足用户对文本内容修改的基本需求。 - 格式处理模块:有
justifyText
(左右对齐)、centerText
(居中对齐)和cancelJustify
(取消对齐)函数,通过对文本中空格等元素的处理来改变文本排版格式,并更新isTextJustified
状态标记。 - 输出与存储模块:
output
函数负责将文本按照当前格式要求输出显示到终端,saveToFile
函数则根据指定的保存模式(覆盖或追加)把文本内容保存到文件中,保障文本处理结果能有效地展示和持久化存储。 - 辅助模块:
clearScreen
函数用于清屏操作(针对 Windows 系统),printMenu
函数用于展示功能菜单,提供清晰友好的用户交互界面,辅助整个程序的使用体验。
- 输入模块:由
详细设计(算法解释)
- 文本输入(
input
与readFromFile
函数):- 算法思路:无论是从终端输入还是从文件读取,都是逐行处理文本内容。对于每行文本,逐个字符进行遍历并添加到一个临时字符串
newLine
中。当newLine
的长度达到 80 时,将其存入lines
向量中,并清空newLine
准备下一段内容的存储;若长度超过 80,则先存入前 80 个字符,然后将剩余部分作为新的newLine
继续处理。最后,如果newLine
还有剩余内容(即不满 80 字符),也将其存入lines
中。这样就完成了文本的分行存储,便于后续各种操作基于行来处理文本。
- 算法思路:无论是从终端输入还是从文件读取,都是逐行处理文本内容。对于每行文本,逐个字符进行遍历并添加到一个临时字符串
- 文本统计功能相关算法:
- 统计各种字符数量(
countAllStats
函数):- 算法思路:通过两层循环遍历
lines
向量中的每一行以及每行中的每个字符。对于每个字符,使用预先定义的isAlpha
和isNum
等判断函数来确定其类型,如果是字母则字母数加 1,如果是数字则数字数加 1,若是空格则空格数加 1,同时总字数也每次加 1,最终统计出文章中各类字符的数量以及总字数情况。
- 算法思路:通过两层循环遍历
- 统计子串出现次数(
countSub
函数):- 大小写敏感情况:同样遍历
lines
中的每一行,在每行中使用string
类的find
方法从当前位置开始查找指定子串。如果找到(即find
方法返回值不是string::npos
),则出现次数加 1,并将查找位置向后移动子串长度个字符继续查找,直到找不到为止。 - 大小写不敏感情况:先将子串和每行文本都转换为小写形式(通过
transform
函数结合tolower
函数实现),然后按照上述类似的查找逻辑进行查找统计,确保在不区分大小写的情况下准确统计子串出现次数。
- 大小写敏感情况:同样遍历
- 统计不同单词个数(
countUnique
函数):- 算法思路:先将文章的每行文本通过
stringstream
进行分割,提取出每个单词并存储到一个新的vector<string>
中。然后对这个单词向量进行排序(使用sort
函数),这样相同的单词就会相邻。接着遍历排序后的单词向量,比较相邻单词是否相同,如果不同则不同单词个数加 1,最终得到文章中不同单词的数量。
- 算法思路:先将文章的每行文本通过
- 统计句子个数(
countSentences
函数):- 算法思路:遍历
lines
中的每一行文本,再逐个字符检查是否为句子结束标点(“.”“?”“!”),如果是则句子个数加 1,通过这样简单的字符匹配统计出文章中句子的总数。
- 算法思路:遍历
- 统计各种字符数量(
- 文本编辑功能相关算法:
- 删除子串(
deleteSub
函数):- 算法思路:遍历
lines
中的每一行文本,在每行中使用find
方法查找指定子串的位置。如果找到且子串位置在该行合法范围内(即pos >= 0
且pos + sub.length() <= line.length()
),则使用erase
方法删除该子串,然后继续从当前位置查找是否还有该子串,直到找不到为止,以此实现将文章中所有匹配的子串删除的功能。
- 算法思路:遍历
- 删除指定行(
deleteLine
函数):- 算法思路:首先判断输入的行号是否在合法范围内(大于等于 1 且小于等于
lineCount
),如果合法则使用vector
的erase
方法删除对应行(通过lines.begin() + lineNumber - 1
定位到要删除的行迭代器位置),同时更新lineCount
减 1,表示行数减少了一行,并且输出相应提示信息;若行号不合法则提示用户重新输入。
- 算法思路:首先判断输入的行号是否在合法范围内(大于等于 1 且小于等于
- 单词首字母大写(
capitalizeWords
函数):- 算法思路:遍历
lines
中的每一行文本,对于每行中的每个字符,通过isspace
函数判断是否为空格,如果是空格则标记下一个字符为新单词的开始(通过newWord
变量标记)。当遇到新单词且字符是字母时(通过isAlpha
函数判断),使用toupper
函数将该字母转换为大写,然后取消新单词标记,以此实现将每个单词首字母大写的功能。
- 算法思路:遍历
- 取消单词首字母大写(
uncapitalizeWords
函数):- 算法思路:与
capitalizeWords
函数类似的遍历逻辑,当遇到空格时标记新单词开始,当遇到新单词且字符是大写字母(通过isAlpha
和isupper
函数判断)时,使用tolower
函数将其转换为小写,从而取消单词首字母的大写状态。
- 算法思路:与
- 删除子串(
- 文本格式处理功能相关算法:
- 文本左右对齐(
justifyText
函数):- 算法思路:首先对每行文本调用
normalizeWhiteSpaces
函数规范空白字符(将连续空白转换为单个空格等),得到处理后的行文本processedLine
并获取其长度。如果长度小于 80,先统计该行空格数量spaceCount
,计算需要额外添加的空格数extraSpaces
(80 减去当前行长度),然后平均分配这些额外空格到各个已有空格间隙中(得到spacesPerGap
),如有剩余空格则依次分配到前面的间隙中。通过stringstream
分割单词,按照分配好的空格数量依次添加单词和空格,构建出左右对齐后的文本行,并更新原lines
中的对应行内容,最后标记isTextJustified
为true
表示文本已对齐。
- 算法思路:首先对每行文本调用
- 文本居中对齐(
centerText
函数):- 算法思路:同样先规范空白字符得到
processedLine
并获取长度,若长度小于 80,统计空格数量后计算额外空格数以及两侧平均分配的空格数spacesPerSide
和剩余空格数remainingSpaces
。先在前面添加相应数量的空格(先处理两侧平均空格,再处理剩余的 1 个空格,如果有的话),然后通过stringstream
分割单词并添加单词和中间的空格,最后再添加后面的空格,构建出居中对齐的文本行并更新原lines
中的行内容,同时标记isTextJustified
为true
。
- 算法思路:同样先规范空白字符得到
- 取消文本对齐(
cancelJustify
函数):- 算法思路:将
isTextJustified
标记设为false
,然后遍历lines
中的每一行文本,逐个字符处理。对于非空格字符直接添加到新构建的originalLine
中;对于空格字符,只添加一个空格,并且跳过后续连续的空格字符,最终将处理后的originalLine
赋值回原lines
中的对应行,以此恢复文本到原始格式状态。
- 算法思路:将
- 文本左右对齐(
- 文本输出(
output
函数):- 算法思路:先输出提示信息 “文章内容如下:”,然后根据
isTextJustified
标记判断文本当前对齐状态。如果处于对齐状态,则直接逐行输出lines
中的内容;如果未对齐,则对每行文本按照字符位置依次输出,避免按 80 字符截断,保证输出原始的文本内容格式。
- 算法思路:先输出提示信息 “文章内容如下:”,然后根据
- 文本保存(
saveToFile
函数):- 算法思路:根据传入的文件名和指定的保存模式(默认为覆盖模式
ios::out
,也可选择追加模式ios::app
)打开文件流ofstream
。如果文件成功打开,则遍历lines
向量,将每行文本写入文件中,并添加换行符,最后关闭文件流,输出相应的保存成功提示信息;若文件打开失败则提示用户检查文件名或文件权限问题。
- 算法思路:根据传入的文件名和指定的保存模式(默认为覆盖模式
countAllStats
函数(统计文章中的字母数、数字数、空格数和总字数)
// 统计文章中的字母数、数字数、空格数和总字数
void LineEditor::countAllStats(int& alphaCount, int& numCount, int& spaceCount, int& totalCount) {
alphaCount = 0;
numCount = 0;
spaceCount = 0;
totalCount = 0;
// 遍历每一行文本
for (const string& line : lines) {
// 遍历每行中的每个字符
for (char c : line) {
// 判断是否为英文字母
if (isAlpha(c)) {
alphaCount++;
}
// 判断是否为数字
else if (isNum(c)) {
numCount++;
}
// 判断是否为空格
else if (c == ' ') {
spaceCount++;
}
// 总字数每次加1
totalCount++;
}
}
}
通过两层嵌套循环,外层遍历存储文本行的 lines
向量,内层遍历每行的字符。针对每个字符,利用 isAlpha
、isNum
函数判断其类型,分别对应地更新字母数、数字数、空格数以及总字数的统计变量。
countSub
函数(统计子串出现次数)
// 统计子串出现次数
int LineEditor::countSub(const string& sub, bool caseSensitive /* = true */) {
int count = 0;
if (caseSensitive) {
// 遍历每一行文本
for (const string& line : lines) {
size_t pos = 0;
// 循环查找子串,直到找不到为止
while (true) {
// 在当前行从pos位置开始查找子串
pos = line.find(sub, pos);
if (pos == string::npos) break;
count++;
// 将查找位置向后移动子串长度个字符,继续查找
pos += sub.length();
}
}
}
else {
string lowerSub = sub;
// 将子串转换为小写形式
transform(lowerSub.begin(), lowerSub.end(), lowerSub.begin(), ::tolower);
for (const string& line : lines) {
string lowerLine = line;
// 将当前行文本转换为小写形式
transform(lowerLine.begin(), lowerLine.end(), lowerLine.begin(), ::tolower);
size_t pos = 0;
while (true) {
// 在小写形式的当前行中查找小写形式的子串
pos = lowerLine.find(lowerSub, pos);
if (pos == string::npos) break;
count++;
pos += lowerSub.length();
}
}
}
return count;
}
根据是否大小写敏感分为两种情况。大小写敏感时,遍历每一行文本,在每行中使用 find
方法从指定位置开始查找子串,找到则出现次数加 1,并将查找位置后移子串长度继续查找,直到找不到(find
返回 string::npos
)为止。大小写不敏感时,先把传入子串和每行文本都转换为小写形式,再按照类似查找逻辑统计出现次数。
countUnique
函数(统计不同单词个数)
// 统计不同单词个数
int LineEditor::countUnique() {
vector<string> words;
// 遍历每一行文本
for (const string& line : lines) {
stringstream ss(line);
string word;
// 利用stringstream分割每行文本,提取出每个单词并存入words向量
while (ss >> word) {
words.push_back(word);
}
}
// 对单词向量进行排序,使得相同单词相邻
sort(words.begin(), words.end());
int uniqueCount = 1;
// 遍历排序后的单词向量,比较相邻单词是否相同来统计不同单词个数
for (size_t i = 1; i < words.size(); ++i) {
if (words[i]!= words[i - 1]) {
uniqueCount++;
}
}
return uniqueCount;
}
首先通过遍历 lines
向量中的每一行文本,利用 stringstream
将每行文本按空格分割成单个单词,并将这些单词依次存入 words
向量中。接着对 words
向量进行排序,这样相同的单词就会相邻排列。然后遍历排序后的 words
向量,通过比较相邻单词是否相同来统计不同单词的个数,初始设为 1(至少有一个单词),若相邻单词不同则个数加 1。
countSentences
函数(统计句子个数)
// 统计句子个数
int LineEditor::countSentences() {
int count = 0;
// 遍历每一行文本
for (const string& line : lines) {
// 遍历每行中的每个字符
for (char c : line) {
// 判断字符是否为句子结束标点(.?!)
if (c == '.' || c == '?' || c == '!') {
count++;
}
}
}
return count;
}
通过两层嵌套循环,外层遍历 lines
向量中的每行文本,内层遍历每行中的每个字符。对于每个字符,判断其是否为句子结束标点(“.”“?”“!”),若是则句子个数统计变量加 1,最终返回统计得到的句子个数。
deleteSub
函数(删除子串)
// 删除子串
void LineEditor::deleteSub(const string& sub) {
for (string& line : lines) {
size_t pos = 0;
// 循环查找子串位置,只要找到且位置合法就删除子串
while ((pos = line.find(sub, pos))!= string::npos) {
if (pos >= 0 && pos + sub.length() <= line.length()) {
line.erase(pos, sub.length());
}
else {
break;
}
}
}
}
遍历存储文本行的 lines
向量中的每一行文本,在每行中使用 find
方法查找指定子串的位置。如果找到子串且其位置在该行文本的合法范围内(即起始位置非负且子串结束位置不超出该行长度),则使用 erase
方法删除该子串,然后继续从当前位置查找是否还有该子串,直到找不到(find
返回 string::npos
)为止,以此实现将文章中所有匹配的子串删除。
deleteLine
函数(删除指定行)
// 删除指定行
void LineEditor::deleteLine(int lineNumber) {
if (lineNumber >= 1 && lineNumber <= lineCount) {
// 利用vector的erase方法删除对应行
lines.erase(lines.begin() + lineNumber - 1);
lineCount--;
cout << "成功删除第 " << lineNumber << " 行。\n";
}
else {
cout << "无效的行号,请重新输入。\n";
}
}
首先判断传入的行号是否在合法范围内(大于等于 1 且小于等于当前记录的行数 lineCount
),如果行号合法,则通过 vector
的 erase
方法定位到要删除的行(利用 lines.begin() + lineNumber - 1
得到对应行的迭代器位置)并删除该行,同时将 lineCount
减 1 表示行数减少了一行,并且输出相应的提示信息告知用户删除成功;若行号不合法,则输出提示信息让用户重新输入。
capitalizeWords
函数(将单词首字母大写)
// 将单词首字母大写
void LineEditor::capitalizeWords() {
for (string& line : lines) {
bool newWord = true;
for (size_t i = 0; i < line.size(); ++i) {
// 判断是否为空格,若是则标记下一个字符为新单词开始
if (isspace(line[i])) {
newWord = true;
}
// 遇到新单词且字符为字母时,将该字母转换为大写
else if (newWord && isAlpha(line[i])) {
line[i] = toupper(line[i]);
newWord = false;
}
}
}
}
遍历 lines
向量中的每一行文本,对于每行文本中的每个字符,通过 isspace
函数判断是否为空格,如果是空格则标记下一个字符为新单词的开始(通过 newWord
变量来标记)。当遇到新单词且该字符是字母(通过 isAlpha
函数判断)时,使用 toupper
函数将该字母转换为大写,然后将 newWord
标记设为 false
,表示当前单词已处理过首字母大写,以此实现将文章中每个单词首字母大写的功能。
uncapitalizeWords
函数(取消单词首字母大写)
// 取消单词首字母大写
void LineEditor::uncapitalizeWords() {
for (string& line : lines) {
bool newWord = true;
for (size_t i = 0; i < line.size(); ++i) {
if (isspace(line[i])) {
newWord = true;
}
// 遇到新单词且字符为大写字母时,将其转换为小写
else if (newWord && isAlpha(line[i]) && isupper(line[i])) {
line[i] = tolower(line[i]);
newWord = false;
}
}
}
}
与 capitalizeWords
函数类似的双层循环遍历逻辑,先通过空格判断新单词开始(通过 newWord
变量标记)。当遇到新单词且该字符是大写字母(通过 isAlpha
和 isupper
函数判断)时,使用 tolower
函数将该字母转换为小写,从而取消单词首字母的大写状态,实现恢复或改变文本格式外观的功能。
justifyText
函数(文本排版 - 左右对齐)
// 文本排版(左右对齐)
void LineEditor::justifyText() {
for (string& line : lines) {
string processedLine = normalizeWhiteSpaces(line);
int lineLength = processedLine.length();
if (lineLength < 80) {
int spaceCount = countSpaces(processedLine);
if (spaceCount > 0) {
int extraSpaces = 80 - lineLength;
int spacesPerGap = extraSpaces / spaceCount;
int remainingSpaces = extraSpaces % spaceCount;
string justifiedLine = "";
stringstream ss(processedLine);
string word;
ss >> word;
justifiedLine += word;
spaceCount--;
while (ss >> word) {
if (spaceCount > 0) {
for (int j = 0; j < spacesPerGap; j++) {
justifiedLine += ' ';
}
if (remainingSpaces > 0) {
justifiedLine += ' ';
remainingSpaces--;
}
spaceCount--;
}
justifiedLine += word;
}
line = justifiedLine;
}
}
}
isTextJustified = true;
}
首先遍历每一行文本,对每行调用 normalizeWhiteSpaces
函数规范空白字符(将连续空白转换为单个空格等),得到处理后的行文本 processedLine
并获取其长度。如果长度小于 80,先通过 countSpaces
函数统计该行空格数量 spaceCount
,接着计算需要额外添加的空格数 extraSpaces
(目标长度 80 减去当前行长度),然后算出平均每个空格间隙要分配的空格数 spacesPerGap
以及剩余空格数 remainingSpaces
。通过 stringstream
分割单词,先添加第一个单词,之后对于后续的每个单词,根据计算好的空格分配情况,先添加相应数量的空格(先按平均分配的 spacesPerGap
个空格添加,再处理剩余空格),再添加单词,构建出左右对齐后的文本行,并更新原 lines
中的对应行内容,最后标记 isTextJustified
为 true
表示文本已对齐。
centerText
函数(文本排版 - 居中对齐)
// 文本排版(居中对齐)
void LineEditor::centerText() {
for (string& line : lines) {
string processedLine = normalizeWhiteSpaces(line);
int lineLength = processedLine.length();
if (lineLength < 80) {
int spaceCount = countSpaces(processedLine);
if (spaceCount > 0) {
int extraSpaces = 80 - lineLength;
int spacesPerSide = extraSpaces / 2;
int remainingSpaces = extraSpaces % 2;
string centeredLine = "";
stringstream ss(processedLine);
string word;
// 先添加空格
for (int j = 0; j < spacesPerSide; j++) {
centeredLine += ' ';
}
if (remainingSpaces > 0) {
centeredLine += ' ';
}
// 添加单词和中间的空格
while (ss >> word) {
centeredLine += word;
if (spaceCount > 0) {
centeredLine += ' ';
spaceCount--;
}
}
for (int j = 0; j < spacesPerSide; j++) {
centeredLine += ' ';
}
line = centeredLine;
}
}
}
isTextJustified = true;
}
同样先遍历每一行文本,对每行调用 normalizeWhiteSpaces
函数规范空白字符得到 processedLine
并获取其长度。若长度小于 80,先统计空格数量 spaceCount
,然后计算额外空格数 extraSpaces
以及两侧平均分配的空格数 spacesPerSide
和剩余空格数 remainingSpaces
。先在构建的 centeredLine
字符串前面添加相应数量的空格(先按平均分配的 spacesPerSide
个空格添加,再处理剩余的 1 个空格,如果有的话),接着通过 stringstream
分割单词并添加单词和中间的空格,最后再添加后面的空格,构建出居中对齐的文本行,并更新原 lines
中的对应行内容,同时标记 isTextJustified
为 true
表示文本已进行居中对齐排版。
cancelJustify
函数(取消文本对齐)
// 取消文本对齐,增加逻辑恢复原始文本内容格式(这里简单处理,如有需要可更复杂完善)
void LineEditor::cancelJustify() {
isTextJustified = false;
for (string& line : lines) {
string originalLine = "";
size_t i = 0;
while (i < line.length()) {
if (line[i]!= ' ') {
originalLine += line[i];
i++;
}
else {
originalLine += line[i];
while (i + 1 < line.length() && line[i + 1] == ' ') {
i++;
}
i++;
}
}
line = originalLine;
}
}
首先将表示文本对齐状态的 isTextJustified
标记设为 false
,然后遍历 lines
向量中的每一行文本。对于每行文本中的每个字符,逐个进行处理,如果字符不是空格,则直接添加到新构建的 originalLine
字符串中;如果字符是空格,则只添加一个空格到 originalLine
中,并且跳过后续连续的空格字符(通过内层循环实现),直到处理完该行所有字符,最后将处理后的 originalLine
赋值回原 lines
中的对应行,以此恢复文本到原始格式状态,取消之前设置的文本对齐效果。
设计与调试过程
- 设计过程:
- 功能规划阶段:根据需求分析确定要实现的文本编辑相关功能,梳理出如输入输出、统计、编辑、格式处理等主要功能模块,并考虑各功能之间的数据交互和依赖关系,例如统计功能需要基于已输入的文本数据进行操作,格式处理后的文本状态会影响输出展示等,以此为基础设计出整体的程序架构和类结构。
- 算法选择与设计阶段:针对每个具体功能,选择合适的算法和数据结构来实现。例如,在统计不同单词个数时选择使用
stringstream
分割单词结合排序算法来去重计数;在文本对齐功能中,通过计算空格数量和合理分配空格来实现排版效果等,精心设计每个功能对应的详细算法流程,确保功能的准确性和高效性。 - 接口设计阶段:设计
LineEditor
类的公有成员函数接口,使其具有清晰明确的功能语义,方便在main
函数中进行调用和功能组合,同时考虑函数参数的合理性和返回值的设置,便于与用户交互以及在内部进行数据传递和状态更新。例如,countSub
函数传入子串和大小写敏感标记,返回子串出现次数;saveToFile
函数传入文件名和保存模式参数,实现不同方式的文件保存等。
- 调试过程:
- 语法错误检查:在编写代码过程中,利用编译器的报错信息及时检查和修正语法错误,如缺少分号、括号不匹配、变量未定义等常见语法问题,确保代码能顺利通过编译阶段。
- 功能测试:
- 单元测试:针对每个功能模块的函数进行单独测试,例如先测试
input
函数能否正确接收并存储用户输入的文本内容,通过手动输入不同情况的文本(如正常文本、超长文本等),检查lines
向量中的存储结果是否符合预期;对countAllStats
函数,构造一些包含不同类型字符的测试文本,验证统计出的字母数、数字数等是否准确。通过这样的单元测试逐步排查各个功能函数的逻辑错误。 - 集成测试:将各个功能函数组合起来,按照实际使用场景通过菜单选择依次执行不同功能,检查功能之间的衔接是否正常,比如先输入文本,然后进行编辑、格式处理,再输出或保存,查看整个流程下来文本的最终状态和结果是否符合预期要求。在这个过程中,特别关注数据在不同功能间传递和修改后是否出现异常情况,如文本对齐后再取消对齐是否能正确恢复原始格式等。
- 单元测试:针对每个功能模块的函数进行单独测试,例如先测试
- 边界情况测试:考虑各种边界情况进行测试,比如在统计功能中,测试空文本的情况,看统计结果是否正确(应该各项统计数都为 0);在删除行功能中,测试删除第一行、最后一行以及超出范围的行号等情况,确保程序在边界条件下也能稳定运行,不会出现崩溃或错误的结果。
- 异常处理测试:检查程序对异常情况的处理是否得当,例如在文件读取和保存时,故意输入不存在的文件名或者没有相应权限的文件名,查看程序是否能正确提示用户相关错误信息,而不是出现未处理的异常导致程序意外终止。
完整代码
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <cctype>
#include <sstream>
#include <iomanip>
#include <limits>
#include <fstream>
using namespace std;
// 判断字符是否为英文字母
bool isAlpha(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
// 判断字符是否为数字
bool isNum(char c) {
return c >= '0' && c <= '9';
}
class LineEditor {
private:
vector<string> lines;
bool isTextJustified = false;
int lineCount = 0; // 用于记录行数
public:
// 输入文章内容(添加自动换行功能)
void input() {
string line;
cout << "请逐行输入文章内容:\n";
cin.ignore(numeric_limits<streamsize>::max(), '\n');
while (getline(cin, line)) {
if (line.empty()) break;
string newLine;
for (size_t i = 0; i < line.length(); ++i) {
newLine += line[i];
if (newLine.length() == 80) {
lines.push_back(newLine);
lineCount++;
newLine.clear();
}
else if (newLine.length() > 80) {
lines.push_back(newLine.substr(0, 80));
lineCount++;
newLine = newLine.substr(80);
}
}
if (!newLine.empty()) {
lines.push_back(newLine);
lineCount++;
}
}
}
// 统计文章中的字母数、数字数、空格数和总字数
void countAllStats(int& alphaCount, int& numCount, int& spaceCount, int& totalCount) {
alphaCount = 0;
numCount = 0;
spaceCount = 0;
totalCount = 0;
for (const string& line : lines) {
for (char c : line) {
if (isAlpha(c)) {
alphaCount++;
}
else if (isNum(c)) {
numCount++;
}
else if (c == ' ') {
spaceCount++;
}
totalCount++;
}
}
}
// 统计子串出现次数
int countSub(const string& sub, bool caseSensitive = true) {
int count = 0;
if (caseSensitive) {
for (const string& line : lines) {
size_t pos = 0;
while (true) {
pos = line.find(sub, pos);
if (pos == string::npos) break;
count++;
pos += sub.length();
}
}
}
else {
string lowerSub = sub;
transform(lowerSub.begin(), lowerSub.end(), lowerSub.begin(), ::tolower);
for (const string& line : lines) {
string lowerLine = line;
transform(lowerLine.begin(), lowerLine.end(), lowerLine.begin(), ::tolower);
size_t pos = 0;
while (true) {
pos = lowerLine.find(lowerSub, pos);
if (pos == string::npos) break;
count++;
pos += lowerSub.length();
}
}
}
return count;
}
// 删除子串
void deleteSub(const string& sub) {
for (string& line : lines) {
size_t pos = 0;
while ((pos = line.find(sub, pos)) != string::npos) {
if (pos >= 0 && pos + sub.length() <= line.length()) {
line.erase(pos, sub.length());
}
else {
break;
}
}
}
}
// 输出文章内容(优化取消对齐后的输出样式)
void output() {
cout << "文章内容如下:\n";
if (isTextJustified) {
for (const string& line : lines) {
cout << line << endl;
}
}
else {
for (const string& line : lines) {
size_t start = 0;
size_t end = line.length(); // 这里修改为直接取整行长度,更符合取消对齐输出原始内容的逻辑
while (start < line.length()) {
cout << line.substr(start, end - start) << endl;
start = end;
end = line.length(); // 同样修改为取整行长度,避免按80字符截断
}
}
}
}
// 将单词首字母大写
void capitalizeWords() {
for (string& line : lines) {
bool newWord = true;
for (size_t i = 0; i < line.size(); ++i) {
if (isspace(line[i])) {
newWord = true;
}
else if (newWord && isAlpha(line[i])) {
line[i] = toupper(line[i]);
newWord = false;
}
}
}
}
// 取消单词首字母大写
void uncapitalizeWords() {
for (string& line : lines) {
bool newWord = true;
for (size_t i = 0; i < line.size(); ++i) {
if (isspace(line[i])) {
newWord = true;
}
else if (newWord && isAlpha(line[i]) && isupper(line[i])) {
line[i] = tolower(line[i]);
newWord = false;
}
}
}
}
// 统计不同单词个数
int countUnique() {
vector<string> words;
for (const string& line : lines) {
stringstream ss(line);
string word;
while (ss >> word) {
words.push_back(word);
}
}
sort(words.begin(), words.end());
int uniqueCount = 1;
for (size_t i = 1; i < words.size(); ++i) {
if (words[i] != words[i - 1]) {
uniqueCount++;
}
}
return uniqueCount;
}
// 删除指定行
void deleteLine(int lineNumber) {
if (lineNumber >= 1 && lineNumber <= lineCount) {
lines.erase(lines.begin() + lineNumber - 1);
lineCount--;
cout << "成功删除第 " << lineNumber << " 行。\n";
}
else {
cout << "无效的行号,请重新输入。\n";
}
}
// 统计句子个数
int countSentences() {
int count = 0;
for (const string& line : lines) {
for (char c : line) {
if (c == '.' || c == '?' || c == '!') {
count++;
}
}
}
return count;
}
// 文本排版(左右对齐)
void justifyText() {
for (string& line : lines) {
string processedLine = normalizeWhiteSpaces(line);
int lineLength = processedLine.length();
if (lineLength < 80) {
int spaceCount = countSpaces(processedLine);
if (spaceCount > 0) {
int extraSpaces = 80 - lineLength;
int spacesPerGap = extraSpaces / spaceCount;
int remainingSpaces = extraSpaces % spaceCount;
string justifiedLine = "";
stringstream ss(processedLine);
string word;
ss >> word;
justifiedLine += word;
spaceCount--;
while (ss >> word) {
if (spaceCount > 0) {
for (int j = 0; j < spacesPerGap; j++) {
justifiedLine += ' ';
}
if (remainingSpaces > 0) {
justifiedLine += ' ';
remainingSpaces--;
}
spaceCount--;
}
justifiedLine += word;
}
line = justifiedLine;
}
}
}
isTextJustified = true;
}
// 文本居中对齐
void centerText() {
for (string& line : lines) {
string processedLine = normalizeWhiteSpaces(line);
int lineLength = processedLine.length();
if (lineLength < 80) {
int spaceCount = countSpaces(processedLine);
if (spaceCount > 0) {
int extraSpaces = 80 - lineLength;
int spacesPerSide = extraSpaces / 2;
int remainingSpaces = extraSpaces % 2;
string centeredLine = "";
stringstream ss(processedLine);
string word;
// 先添加空格
for (int j = 0; j < spacesPerSide; j++) {
centeredLine += ' ';
}
if (remainingSpaces > 0) {
centeredLine += ' ';
}
// 添加单词和中间的空格
while (ss >> word) {
centeredLine += word;
if (spaceCount > 0) {
centeredLine += ' ';
spaceCount--;
}
}
for (int j = 0; j < spacesPerSide; j++) {
centeredLine += ' ';
}
line = centeredLine;
}
}
}
isTextJustified = true;
}
// 取消文本对齐,增加逻辑恢复原始文本内容格式(这里简单处理,如有需要可更复杂完善)
void cancelJustify() {
isTextJustified = false;
for (string& line : lines) {
string originalLine = "";
size_t i = 0;
while (i < line.length()) {
if (line[i] != ' ') {
originalLine += line[i];
i++;
}
else {
originalLine += line[i];
while (i + 1 < line.length() && line[i + 1] == ' ') {
i++;
}
i++;
}
}
line = originalLine;
}
}
// 规范处理文本行中的空白字符,将连续空白转换为单个空格等
string normalizeWhiteSpaces(const string& line) {
string result = "";
for (size_t i = 0; i < line.length(); ++i) {
if (isspace(line[i])) {
result += ' ';
while (i + 1 < line.length() && isspace(line[i + 1])) {
i++;
}
}
else {
result += line[i];
}
}
return result;
}
// 统计一行中的空格数量
int countSpaces(const string& line) {
int count = 0;
for (char c : line) {
if (c == ' ') {
count++;
}
}
return count;
}
// 从文件中读取文章内容
void readFromFile(const string& filename) {
ifstream inputFile(filename);
if (inputFile.is_open()) {
string line;
while (getline(inputFile, line)) {
if (line.empty()) continue;
string newLine;
for (size_t i = 0; i < line.length(); ++i) {
newLine += line[i];
if (newLine.length() == 80) {
lines.push_back(newLine);
lineCount++;
newLine.clear();
}
else if (newLine.length() > 80) {
lines.push_back(newLine.substr(0, 80));
lineCount++;
newLine = newLine.substr(80);
}
}
if (!newLine.empty()) {
lines.push_back(newLine);
lineCount++;
}
}
inputFile.close();
cout << "文章内容已成功从 " << filename << " 文件中读取。" << endl;
}
else {
cout << "无法打开文件 " << filename << " 进行读取,请检查文件名或文件权限。" << endl;
}
}
// 保存文章内容到文件,可指定保存模式(默认为覆盖模式)
void saveToFile(const string& filename, ios::openmode mode = ios::out) {
ofstream outputFile(filename, mode);
if (outputFile.is_open()) {
for (const string& line : lines) {
outputFile << line << endl;
}
outputFile.close();
cout << "文章内容已成功保存到 " << filename << " 文件中。" << endl;
}
else {
cout << "无法打开文件 " << filename << " 进行保存,请检查文件名或文件权限。" << endl;
}
}
};
// 清屏函数(适用于Windows系统)
void clearScreen() {
system("cls");
}
// 打印菜单,调整后的更合理布局和分组方式
void printMenu() {
const string menu =
"\t*************************************\n"
"\t** lx简单行编辑器菜单 **\n"
"\t** 1. 输入文章内容 **\n"
"\t** 2. 输出文章内容 **\n"
"\t** 3. 统计信息 **\n"
"\t** 3.1 统计子串出现次数 **\n"
"\t** 3.2 统计不同单词个数 **\n"
"\t** 3.3 统计句子个数 **\n"
"\t** 3.4 显示详细字数统计信息 **\n"
"\t** 4. 文本格式处理 **\n"
"\t** 4.1 单词首字母大写 **\n"
"\t** 4.2 取消单词首字母大写 **\n"
"\t** 4.3 文本左右对齐 **\n"
"\t** 4.4 文本居中对齐 **\n"
"\t** 4.5 取消文本对齐 **\n"
"\t** 5. 内容编辑 **\n"
"\t** 5.1 删除子串 **\n"
"\t** 5.2 删除行 **\n"
"\t** 6. 清屏 **\n"
"\t** 7. 从文件读取文章内容 **\n"
"\t** 8. 保存文章内容(可指定模式) **\n"
"\t** 0. 退出程序 **\n"
"\t*************************************\n";
cout << menu;
}
int main() {
LineEditor editor;
int choice;
do {
printMenu();
cout << "请输入你的选择: ";
cin >> choice;
switch (choice) {
case 1:
editor.input();
break;
case 2:
editor.output();
break;
case 3: {
cout << "请选择具体统计功能:\n";
cout << "1. 统计子串出现次数\n";
cout << "2. 统计不同单词个数\n";
cout << "3. 统计句子个数\n";
cout << "4. 显示详细字数统计信息\n";
int subChoice;
cin >> subChoice;
switch (subChoice) {
case 1: {
string sub;
cout << "请输入要统计出现次数的字符串: ";
cin.ignore(numeric_limits<streamsize>::max(), '\n');
getline(cin, sub);
bool sensitive;
cout << "是否大小写敏感(1表示是,0表示否):";
cin >> sensitive;
if (sensitive != 0 && sensitive != 1) {
cout << "输入的选择不合法,请重新输入,只能输入0或1。" << endl;
break;
}
int count = editor.countSub(sub, sensitive);
cout << "字符串 \"" << sub << "\" 在文章中出现的次数为: " << count << endl;
break;
}
case 2: {
int count = editor.countUnique();
cout << "文章中不同单词的个数为: " << count << endl;
break;
}
case 3: {
int count = editor.countSentences();
cout << "文章中句子的个数为: " << count << endl;
break;
}
case 4: {
int alphaCount, numCount, spaceCount, totalCount;
editor.countAllStats(alphaCount, numCount, spaceCount, totalCount);
double alphaPercent = (double)alphaCount / totalCount * 100;
double numPercent = (double)numCount / totalCount * 100;
double spacePercent = (double)spaceCount / totalCount * 100;
cout << "文章字数统计信息如下:\n";
cout << "字母数: " << alphaCount << " (" << fixed << setprecision(2) << alphaPercent << "%)\n";
cout << "数字数: " << numCount << " (" << fixed << setprecision(2) << numPercent << "%)\n";
cout << "空格数: " << spaceCount << " (" << fixed << setprecision(2) << spacePercent << "%)\n";
cout << "总字数: " << totalCount << endl;
break;
}
default:
cout << "无效的选择,请重新输入。" << endl;
break;
}
break;
}
case 4: {
cout << "请选择具体文本格式处理功能:\n";
cout << "1. 单词首字母大写\n";
cout << "2. 取消单词首字母大写\n";
cout << "3. 文本左右对齐\n";
cout << "4. 文本居中对齐\n";
cout << "5. 取消文本对齐\n";
int formatChoice;
cin >> formatChoice;
switch (formatChoice) {
case 1:
editor.capitalizeWords();
editor.output();
break;
case 2:
editor.uncapitalizeWords();
editor.output();
break;
case 3:
editor.justifyText();
editor.output();
break;
case 4:
editor.centerText();
editor.output();
break;
case 5:
editor.cancelJustify();
cout << "已取消文本对齐。" << endl;
editor.output();
break;
default:
cout << "无效的选择,请重新输入。" << endl;
break;
}
break;
}
case 5: {
cout << "请选择具体内容编辑功能:\n";
cout << "1. 删除子串\n";
cout << "2. 删除行\n";
int editChoice;
cin >> editChoice;
switch (editChoice) {
case 1: {
string sub;
cout << "请输入要删除的子串: ";
cin.ignore(numeric_limits<streamsize>::max(), '\n');
getline(cin, sub);
editor.deleteSub(sub);
editor.output();
break;
}
case 2: {
int lineNumber;
cout << "请输入要删除的行号:";
cin >> lineNumber;
editor.deleteLine(lineNumber);
editor.output();
break;
}
default:
cout << "无效的选择,请重新输入。" << endl;
break;
}
break;
}
case 6:
clearScreen();
break;
case 7: {
string filename;
cout << "请输入要读取的文件名(包含扩展名,如.txt):";
cin.ignore(numeric_limits<streamsize>::max(), '\n');
getline(cin, filename);
editor.readFromFile(filename);
break;
}
case 8: {
string filename;
cout << "请输入要保存的文件名(包含扩展名,如.txt):";
cin.ignore(numeric_limits<streamsize>::max(), '\n');
getline(cin, filename);
int saveChoice;
cout << "请选择保存模式(1表示覆盖,2表示追加):";
cin >> saveChoice;
ios::openmode mode = (saveChoice == 1) ? ios::out : ios::app;
editor.saveToFile(filename, mode);
break;
}
case 0:
cout << "感谢使用,程序已退出。" << endl;
break;
default:
cout << "无效的选择,请重新输入。" << endl;
break;
}
cout << endl;
} while (choice != 0);
return 0;
}