12、C++项目实战:简易文本编辑器
文本编辑器,听起来是不是有点「杀鸡用牛刀」的感觉?
其实不然。我做了这么多年C++开发,发现很多新手对文件流、字符串处理、控制台交互这些基础操作,往往停留在「会用」但「不熟」的阶段。这个项目,说白了就是把这些零散知识点串起来,做一个真正能用的东西。
你想想看,一个文本编辑器要做什么?打开文件、显示内容、编辑、保存。就这么简单?嗯,核心功能确实就这些。但实现起来,坑可不少。
项目目标
我们要做一个控制台下的简易文本编辑器。它不需要图形界面,但必须能完成以下工作:
- 读取并显示文本文件
- 支持行内编辑(插入、删除、修改字符)
- 支持行操作(插入行、删除行)
- 将修改后的内容保存回文件
我个人习惯把这种项目叫做「最小可行产品」。功能不多,但五脏俱全。
核心数据结构设计
编辑器最核心的问题:怎么存文本?
用 std::string 存整篇?不行。插入和删除操作会频繁移动大量字符,效率太低。
用 std::vector<std::string> 按行存储?这个方案靠谱。每行是一个字符串,行操作就是 vector 的元素操作。我在项目中遇到过类似场景,处理日志文件时就是这么干的。
核心数据结构:
std::vector<std::string> lines; // 按行存储文本
std::string filename; // 当前打开的文件名
int cursor_row = 0; // 当前光标行号(从0开始)
int cursor_col = 0; // 当前光标列号(从0开始)
为什么用 vector?因为它的随机访问是 O(1),插入和删除中间元素虽然要移动后续元素,但每行是一个独立的 string,移动的是指针而不是整行字符,效率可以接受。
文件读写模块
这部分其实没什么玄乎的。C++ 的 fstream 库足够用了。
读取文件:
bool loadFile(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) {
std::cerr << "无法打开文件: " << path << std::endl;
return false;
}
lines.clear();
std::string line;
while (std::getline(file, line)) {
lines.push_back(line);
}
file.close();
filename = path;
return true;
}
这里有个细节:std::getline 默认会丢弃换行符。所以读进来之后,每行末尾是没有 \n 的。保存的时候要自己加上。
保存文件:
bool saveFile() {
std::ofstream file(filename);
if (!file.is_open()) {
std::cerr << "无法写入文件: " << filename << std::endl;
return false;
}
for (const auto& line : lines) {
file << line << '\n'; // 手动加换行
}
file.close();
return true;
}
我曾经犯过一个低级错误:保存时忘了加换行符,结果整个文件变成了一行。嗯,调试了半天才发现。
编辑操作实现
编辑操作分两类:行内编辑和行操作。
行内编辑
说白了就是在当前行的字符串上做增删改。
- 插入字符:
lines[cursor_row].insert(cursor_col, 1, ch);然后cursor_col++ - 删除字符:
lines[cursor_row].erase(cursor_col, 1); - 修改字符:
lines[cursor_row][cursor_col] = ch;
你可能会问:如果光标在行尾怎么办?嗯,插入操作在行尾是允许的,string 的 insert 在末尾也能正常工作。删除操作如果光标在行尾,那就什么都不做。
行操作
- 插入空行:
lines.insert(lines.begin() + cursor_row + 1, ""); - 删除当前行:
lines.erase(lines.begin() + cursor_row); - 合并下一行:
lines[cursor_row] += lines[cursor_row + 1]; lines.erase(lines.begin() + cursor_row + 1);
避坑指南: 删除行之后,光标位置要重新计算。如果删的是最后一行,光标要上移一行。我曾经没处理这个边界,导致程序崩溃了好几次。
显示与交互
控制台下的显示,我建议用最朴素的方式:每次刷新时清屏,然后重新打印所有行。
void display() {
system("clear"); // Windows 下用 system("cls")
for (int i = 0; i < lines.size(); ++i) {
if (i == cursor_row) {
std::cout << "> " << lines[i] << " <" << std::endl;
} else {
std::cout << " " << lines[i] << std::endl;
}
}
std::cout << "行:" << cursor_row+1 << " 列:" << cursor_col+1 << std::endl;
}
为什么每次都要清屏重绘?因为控制台不像图形界面可以局部刷新。清屏重绘虽然粗暴,但实现简单,而且对于小文件来说速度完全够用。
命令系统设计
用户怎么操作?我设计了一套简单的命令:
| 命令 | 功能 |
|---|---|
:q | 退出编辑器 |
:w | 保存文件 |
:wq | 保存并退出 |
:o 文件名 | 打开另一个文件 |
i | 进入插入模式(在光标前插入) |
x | 删除光标处字符 |
dd | 删除当前行 |
o | 在当前行下方插入空行 |
h/j/k/l | 移动光标(左/下/上/右) |
这个命令风格明显借鉴了 Vim。为什么?因为我自己用 Vim 习惯了,而且这种模式在控制台下确实高效。你完全可以改成自己习惯的快捷键。
SVG 结构图:简易文本编辑器核心模块
完整代码框架
我把核心框架贴出来,你感受一下整体结构:
class TextEditor {
private:
std::vector<std::string> lines;
std::string filename;
int cursor_row, cursor_col;
public:
TextEditor() : cursor_row(0), cursor_col(0) {}
bool loadFile(const std::string& path);
bool saveFile();
void moveCursorUp();
void moveCursorDown();
void moveCursorLeft();
void moveCursorRight();
void insertChar(char ch);
void deleteChar();
void insertLine();
void deleteLine();
void display();
void run(); // 主循环
};
这个框架把数据和操作封装在一起,清晰明了。你可以在 run() 里实现命令解析循环,根据用户输入调用对应的成员函数。
避坑总结
- 光标越界: 每次移动光标后,都要检查是否超出当前行的长度。我建议写一个
clampCursor()函数统一处理。 - 空文件处理: 如果文件为空,
lines是空的。此时插入字符前要先插入一个空行。 - 编码问题: 这个编辑器只支持纯 ASCII 文本。中文等宽字符会出问题,因为
char是单字节的。 - 性能问题: 如果文件有上万行,每次清屏重绘会闪烁。可以用
ncurses库优化,但那是进阶内容了。
我的建议: 先把这个基础版本跑通,然后你可以尝试加功能:比如撤销/重做、搜索替换、语法高亮。每加一个功能,你对 C++ 的理解就会深一层。
这个项目看起来简单,但把文件 I/O、容器操作、控制台交互、状态管理这些知识点都串起来了。做完之后,你会发现自己对 C++ 的「手感」完全不一样了。
公众号:蓝海资料掘金营,微信deep3321