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 结构图:简易文本编辑器核心模块

简易文本编辑器核心模块 主控制循环 文件读写模块 编辑操作模块 显示与交互模块 loadFile() / saveFile() 行内编辑 / 行操作 display() / 命令解析 核心数据:vector<string> lines

完整代码框架

我把核心框架贴出来,你感受一下整体结构:

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