C++面向对象编程实践:从零实现命令行文本编辑器

news2026/5/14 15:59:38
1. 项目概述与核心价值最近在整理硬盘翻出来一个大学时期的老项目——一个用C写的命令行文本编辑器。这个项目当时是为了完成《面向对象程序设计》课程的实验作业而做的名字就叫“Cpp_OOP_Labs”。现在回头看虽然代码有些稚嫩但里面几乎涵盖了从C基础到面向对象核心思想的所有关键知识点是一个非常好的学习样本。如果你正在学习C尤其是对“类”、“继承”、“多态”这些概念感到抽象或者对“指针”和“动态内存分配”感到头疼那么这个项目的拆解可能会给你带来不少启发。这个编辑器我们姑且叫它LineEditor功能很简单在一个虚拟的“文档”里你可以插入行、删除行、显示所有行以及进行简单的字符串查找。但它麻雀虽小五脏俱全。为了实现这些功能我不得不去设计类、管理对象生命周期、处理内存还要考虑如何让代码结构更清晰。整个过程就像搭积木从最基础的变量和函数开始一步步构建出完整的面向对象体系。接下来我会把这个项目的实现思路、关键代码、以及当时踩过的坑毫无保留地分享出来。无论你是刚接触C的新手还是想巩固OOP基础的同学相信都能从中找到可以直接“抄作业”的实用代码和避坑指南。2. 项目整体设计与思路拆解2.1 核心需求与功能定义首先我们需要明确这个命令行编辑器要做什么。作为课程实验它的核心需求不是实现一个像Vim或Nano那样功能复杂的编辑器而是通过实现一个具体的应用来练习和演示特定的C与OOP概念。因此我将其功能精简为以下四个核心操作插入行Insert在文档的指定行号位置插入一行文本。删除行Delete删除文档中指定行号的文本。显示文档Display按顺序显示当前文档中的所有行。查找文本Search在文档中查找包含特定子串的所有行并显示其行号。这个设计看似简单却暗含了几个关键的技术挑战如何动态地管理一个大小会变化的“文档”如何高效地根据行号进行插入和删除如何组织代码才能清晰地体现面向对象的思想这些问题的答案直接决定了后续的类设计和实现策略。2.2 技术方案选型与背后的“为什么”面对上述需求有几种实现路径。最直接的想法可能是用一个std::vectorstd::string。这当然可以而且对于这个作业来说可能更简单。但当时课程的要求是必须显式地使用指针和动态内存分配来管理字符串数组目的是为了深入理解内存管理的原理。这是一个非常重要的教学点也是很多同学从“写代码”到“理解计算机如何工作”的关键一步。因此我放弃了使用STL容器的“捷径”选择了更底层但也更能锻炼能力的方案文档存储使用一个char**指向字符指针的指针也就是一个“字符串数组”。数组中的每个元素char*指向一块动态分配的、存储单行文本的内存。容量管理手动维护这个数组的“当前行数”和“总容量”。当插入新行导致行数超过容量时需要动态分配一个更大的新数组将旧数据拷贝过去然后释放旧数组。字符串操作使用C风格的字符串函数strcpy,strlen,strstr等来处理文本的复制、查找和比较。选择这个方案主要有三个考量教学目的优先直接操作指针和内存能最深刻地理解“对象在内存中如何布局”、“动态分配与释放的时机”、“深拷贝与浅拷贝的区别”这些核心概念。这些都是理解C乃至其他系统编程语言的基石。理解成本与收益虽然初期实现比用vector麻烦但一旦实现成功对指针和内存的理解会上升一个维度。后续再使用vector或string时你会清楚地知道它们背后在帮你做什么用起来会更加得心应手也更能避免一些隐藏的陷阱比如迭代器失效。为OOP设计铺路这种需要精细控制资源内存的场景正是引入类Class和资源获取即初始化RAII理念的绝佳舞台。我们可以设计一个Document类将char**、行数、容量这些数据“封装”起来并通过构造函数、析构函数、拷贝控制成员来确保内存管理的正确性从而让代码更安全、更易维护。3. 核心类设计与OOP实践3.1 Document类的封装Encapsulation封装是OOP的第一道屏障它的目的是将数据和对数据的操作捆绑在一起并对外隐藏实现的细节。对于我们的编辑器核心的数据就是一个动态的字符串数组。我们将其封装进一个Document类。// document.h #ifndef DOCUMENT_H #define DOCUMENT_H class Document { private: char** lines; // 指向字符串数组的指针 int lineCount; // 当前存储的有效行数 int capacity; // 当前数组的总容量 // 私有辅助函数 void ensureCapacity(int newLineCount); void shiftLinesDown(int fromIndex); void shiftLinesUp(int fromIndex); public: // 构造函数与析构函数 Document(int initialCapacity 10); ~Document(); // 禁止拷贝初步设计后续会改进 Document(const Document) delete; Document operator(const Document) delete; // 核心功能接口 bool insertLine(int lineNumber, const char* text); bool deleteLine(int lineNumber); void display() const; void search(const char* keyword) const; // 获取状态 int getLineCount() const { return lineCount; } }; #endif // DOCUMENT_H设计解析与注意事项私有数据成员char** lines这是我们的核心数据一个指向动态分配的char*数组的指针。每个char*又指向一块存储单行文本的动态内存。int lineCount记录当前有多少行有效文本。它总是小于等于capacity。int capacity记录lines数组当前最多能容纳多少行文本的指针。当lineCount capacity时下一次插入就需要扩容。私有辅助函数将这些函数设为私有是因为它们只服务于类的内部实现外部调用者不需要也不应该关心。例如ensureCapacity用于在插入前检查并扩容shiftLinesDown/Up用于在插入或删除行时批量移动后续行的指针为新增行腾出空间或填补删除行的空缺。这体现了“信息隐藏”。构造函数与析构函数关键构造函数Document(int initialCapacity)负责初始化。它动态分配一个大小为initialCapacity的char*数组并将lines指向它同时将lineCount设为0。这里分配的是指针数组每一行的文本内存将在插入时分配。析构函数~Document()这是内存安全的核心。它必须负责释放所有申请的内存。流程是先循环释放lines数组中每一个非空的char*即每一行的文本然后再释放lines这个指针数组本身。忘记释放任何一块内存都会导致内存泄漏。禁用拷贝初版我最初使用了 delete来禁止拷贝构造和拷贝赋值。为什么因为默认的拷贝行为是“浅拷贝”它只会复制lines指针的值导致两个Document对象指向同一块内存。那么当一个对象被销毁释放了内存后另一个对象的lines就变成了“悬空指针”再次使用或销毁会导致未定义行为通常是程序崩溃。这是一个经典的陷阱。在后续的“高级话题”中我们会讨论如何实现正确的“深拷贝”。公共接口这些函数定义了用户main函数可以与文档交互的所有方式。它们接收参数调用私有辅助函数和操作私有数据并返回结果。注意display和search被声明为const因为它们不修改对象的状态。实操心得头文件守卫代码中的#ifndef DOCUMENT_H...#endif称为“头文件守卫”或“包含守卫”。它的作用是防止同一个头文件被多次包含进同一个源文件从而避免重复定义类或函数导致的编译错误。这是编写C头文件时必须养成的习惯。3.2 核心成员函数的实现要点接下来我们深入看看几个关键成员函数的具体实现这里充满了指针操作的细节。// document.cpp #include “document.h” #include cstring // for strcpy, strlen, strstr #include iostream using namespace std; Document::Document(int initialCapacity) : capacity(initialCapacity), lineCount(0) { lines new char*[capacity]; // 分配指针数组 for (int i 0; i capacity; i) { lines[i] nullptr; // 初始化所有指针为空 } } Document::~Document() { for (int i 0; i lineCount; i) { delete[] lines[i]; // 释放每一行的文本内存 } delete[] lines; // 释放指针数组本身 } void Document::ensureCapacity(int requiredCapacity) { if (requiredCapacity capacity) return; // 常见的扩容策略新容量为旧容量的1.5倍或2倍避免频繁扩容。 int newCapacity capacity * 2; while (newCapacity requiredCapacity) { newCapacity * 2; } char** newLines new char*[newCapacity]; // 拷贝旧指针 for (int i 0; i lineCount; i) { newLines[i] lines[i]; } // 初始化新增部分为空指针 for (int i lineCount; i newCapacity; i) { newLines[i] nullptr; } delete[] lines; // 释放旧的指针数组 lines newLines; capacity newCapacity; } bool Document::insertLine(int lineNumber, const char* text) { // 参数检查行号必须在0到lineCount之间允许在末尾插入 if (lineNumber 0 || lineNumber lineCount) { cerr “错误行号 ” lineNumber “ 超出范围 (0-” lineCount “).\n”; return false; } // 确保有足够空间容纳新的一行 ensureCapacity(lineCount 1); // 为插入点之后的所有行腾出空间指针向后移动一位 shiftLinesDown(lineNumber); // 为新文本分配内存并拷贝 // 1 是为了存储字符串结尾的 ‘\0’ lines[lineNumber] new char[strlen(text) 1]; strcpy(lines[lineNumber], text); lineCount; return true; } void Document::shiftLinesDown(int fromIndex) { // 从最后一行开始到插入位置结束依次向后移动指针 for (int i lineCount - 1; i fromIndex; --i) { lines[i 1] lines[i]; } // 插入位置现在“空”出来了但lines[fromIndex]目前是旧值会被insertLine覆盖 } bool Document::deleteLine(int lineNumber) { if (lineNumber 0 || lineNumber lineCount) { cerr “错误行号 ” lineNumber “ 无效.\n”; return false; } // 释放要删除的那一行的内存 delete[] lines[lineNumber]; // 将删除点之后的所有行指针向前移动一位覆盖被删除的行 shiftLinesUp(lineNumber 1); // 最后一行的位置现在重复了将其置为空 lines[lineCount - 1] nullptr; lineCount--; return true; } void Document::shiftLinesUp(int fromIndex) { // 从被删除行的下一行开始依次向前移动指针 for (int i fromIndex; i lineCount; i) { lines[i - 1] lines[i]; } }关键点解析与避坑指南内存分配与释放必须配对new[]对应delete[]new对应delete。在insertLine中我们使用new char[strlen(text) 1]为文本分配内存这是一个数组分配所以在析构函数中必须用delete[] lines[i]来释放。而lines本身也是一个数组new char*[capacity]所以用delete[] lines释放。混用会导致运行时错误。字符串拷贝必须包含终止符strlen(text)返回的是文本长度不包含结尾的\0。我们在分配内存时1就是为了给这个终止符留出空间。strcpy会负责连同\0一起拷贝过去。忘记1是初学者常见的错误会导致拷贝越界或字符串操作异常。扩容策略ensureCapacity函数实现了动态扩容。直接申请刚好所需的大小requiredCapacity在理论上最省内存但在频繁插入的场景下会导致多次重新分配和拷贝realloc的模拟性能低下。常见的做法是按倍数扩容这里是2倍。这以一定的空间浪费为代价换取了摊还常数时间的插入性能这是std::vector等现代容器采用的策略。指针移动的边界shiftLinesDown和shiftLinesUp函数中循环的起始和终止索引需要仔细推敲。一个有用的调试方法是画图。假设lines是一个盒子数组每个盒子装着一个指针气球。插入时从最后一个有气球的盒子开始把气球放到下一个空盒子里直到为插入点腾出空盒。删除时把删除点后面盒子的气球依次往前挪一个盒子。画图能极大避免差一错误Off-by-one error。4. 主程序与用户交互实现有了坚实的Document类主程序就变得清晰而简单。它的职责是创建一个Document对象并提供一个循环来读取用户命令然后调用相应的类方法。// main.cpp #include “document.h” #include iostream #include sstream #include string using namespace std; void printHelp() { cout “\n 简单行编辑器 \n”; cout “命令:\n”; cout ” i [行号] [文本] – 在指定行号插入文本行号从0开始\n”; cout ” d [行号] – 删除指定行\n”; cout ” p – 显示所有行\n”; cout ” s [关键词] – 搜索包含关键词的行\n”; cout ” q – 退出\n”; cout ” h – 显示此帮助\n”; cout “\n”; } int main() { Document doc; // 使用默认容量创建文档对象 string commandLine; printHelp(); while (true) { cout “\n “; getline(cin, commandLine); // 读取整行命令 if (commandLine.empty()) continue; istringstream iss(commandLine); char cmd; iss cmd; switch (cmd) { case ‘i’: { // 插入 int lineNum; string text; iss lineNum; // 获取行号后的剩余部分作为插入文本 getline(iss, text); // 去除文本首部的空格 if (!text.empty() text[0] ‘ ‘) { text.erase(0, 1); } if (doc.insertLine(lineNum, text.c_str())) { cout “在第 ” lineNum “ 行插入成功.\n”; } break; } case ‘d’: { // 删除 int lineNum; iss lineNum; if (doc.deleteLine(lineNum)) { cout “第 ” lineNum “ 行已删除.\n”; } break; } case ‘p’: { // 显示 doc.display(); break; } case ‘s’: { // 搜索 string keyword; getline(iss, keyword); if (!keyword.empty() keyword[0] ‘ ‘) { keyword.erase(0, 1); } if (!keyword.empty()) { doc.search(keyword.c_str()); } else { cout “请输入搜索关键词.\n”; } break; } case ‘q’: { // 退出 cout “退出编辑器。\n”; return 0; } case ‘h’: { // 帮助 printHelp(); break; } default: { cout “未知命令 ‘” cmd “‘。输入 ‘h’ 查看帮助。\n”; break; } } } return 0; }交互逻辑的细节与技巧使用getline读取整行命令这允许我们在插入和搜索命令中包含空格。例如命令i 1 Hello World可以被完整读取。使用istringstream解析命令这是一个非常方便的工具它允许我们像从cin读取一样从一个字符串中提取数据。我们先提取命令字符再根据命令提取行号或关键词。处理文本参数的前导空格iss lineNum会读取行号但不会消耗后面的空格。当我们用getline(iss, text)读取剩余部分时如果用户输入了i 1 Hello那么text的第一个字符会是空格。因此我们需要检查并移除这个前导空格。这是一个常见的输入处理细节。清晰的用户反馈每个操作成功后都给出明确的提示“插入成功”、“已删除”失败时通过Document::insertLine内部的cerr输出错误信息。良好的交互能提升使用体验。5. 从基础到进阶深入OOP与内存管理5.1 实现拷贝控制Rule of Three前面我们禁用了拷贝但这限制了Document对象的用途。一个完整的、管理动态资源的类应该遵循“三法则”如果一个类需要自定义析构函数那么它几乎肯定也需要自定义拷贝构造函数和拷贝赋值运算符。让我们为Document类实现深拷贝// 在document.h的public部分添加声明 Document(const Document other); // 拷贝构造函数 Document operator(const Document other); // 拷贝赋值运算符 // 在document.cpp中实现 Document::Document(const Document other) : capacity(other.capacity), lineCount(other.lineCount) { lines new char*[capacity]; for (int i 0; i capacity; i) { lines[i] nullptr; // 初始化新数组 } // 深拷贝为每一行文本分配新内存并复制内容 for (int i 0; i lineCount; i) { if (other.lines[i] ! nullptr) { lines[i] new char[strlen(other.lines[i]) 1]; strcpy(lines[i], other.lines[i]); } } } Document Document::operator(const Document other) { // 1. 防止自赋值 (a a) if (this other) { return *this; } // 2. 释放当前对象持有的资源 for (int i 0; i lineCount; i) { delete[] lines[i]; } delete[] lines; // 3. 分配新资源并拷贝数据与拷贝构造函数逻辑类似 capacity other.capacity; lineCount other.lineCount; lines new char*[capacity]; for (int i 0; i capacity; i) { lines[i] nullptr; } for (int i 0; i lineCount; i) { if (other.lines[i] ! nullptr) { lines[i] new char[strlen(other.lines[i]) 1]; strcpy(lines[i], other.lines[i]); } } // 4. 返回当前对象的引用以支持链式赋值 (a b c) return *this; }实现拷贝赋值运算符的要点Copy-and-Swap惯用法上面的实现是基础版本但它有缺点代码重复与拷贝构造函数类似且如果在分配新资源时失败例如内存不足当前对象的状态已被破坏不再安全。更健壮、更现代的做法是“拷贝并交换”惯用法。它利用拷贝构造函数和一个交换成员函数能写出异常安全且简洁的代码。// 首先添加一个交换成员函数 void Document::swap(Document other) noexcept { using std::swap; swap(lines, other.lines); swap(lineCount, other.lineCount); swap(capacity, other.capacity); } // 然后用“拷贝-交换”实现赋值运算符 Document Document::operator(Document other) { // 注意这里参数是值传递会调用拷贝构造函数 swap(other); // 将当前对象的内容与这个局部副本交换 return *this; } // 函数结束局部副本other被销毁其析构函数会释放我们旧的资源。这种方式极其优雅它通过传值参数自动完成了资源的拷贝利用了拷贝构造函数然后通过交换获得了新资源并让局部对象在离开作用域时自动清理旧资源。代码简洁且异常安全。5.2 引入继承与多态设计一个“可撤销”的编辑器面向对象更强大的特性在于建立类之间的关系。假设我们想为编辑器增加一个“撤销”功能。我们可以通过继承和多态来实现。首先定义一个抽象基类Command表示一个可执行、可撤销的操作。// command.h #ifndef COMMAND_H #define COMMAND_H class Document; // 前向声明 class Command { public: virtual ~Command() default; // 虚析构函数确保正确释放派生类对象 virtual void execute(Document doc) 0; // 执行命令 virtual void undo(Document doc) 0; // 撤销命令 virtual const char* getName() const 0; // 获取命令名称 }; #endif // COMMAND_H然后为“插入”和“删除”操作创建具体的命令类。// insert_command.h #ifndef INSERT_COMMAND_H #define INSERT_COMMAND_H #include “command.h” #include string class InsertCommand : public Command { private: int lineNumber; std::string text; // 使用std::string更安全方便 public: InsertCommand(int line, const std::string txt); void execute(Document doc) override; void undo(Document doc) override; const char* getName() const override { return “Insert”; } }; #endif // INSERT_COMMAND_H// insert_command.cpp #include “insert_command.h” #include “document.h” InsertCommand::InsertCommand(int line, const std::string txt) : lineNumber(line), text(txt) {} void InsertCommand::execute(Document doc) { // 调用Document的插入功能 // 注意这里需要修改Document的insertLine使其返回bool或成功时记录状态。 // 为了简化我们假设总是成功并在undo时知道要删除哪一行。 doc.insertLine(lineNumber, text.c_str()); } void InsertCommand::undo(Document doc) { // 撤销插入就是删除那一行 // 这里有一个问题如果插入后又插入了其他行行号可能变化。 // 一个更健壮的实现需要在Command内部保存一个唯一标识符如行ID而不是行号。 // 本例为演示多态简化处理。 doc.deleteLine(lineNumber); }类似地可以实现DeleteCommand。最后在主程序中我们可以维护一个Command指针的栈例如用std::vectorstd::unique_ptrCommand用来保存历史命令。当用户执行操作时创建对应的命令对象执行它并压入栈。当用户请求撤销时从栈顶弹出命令并调用其undo方法。多态的优势通过基类Command的指针或引用我们可以统一管理所有不同类型的命令。新增一个“替换”命令只需要再创建一个ReplaceCommand类主程序的命令历史管理代码完全不用修改。这就是“对扩展开放对修改关闭”的开闭原则OCP的一个简单体现。5.3 智能指针迈向现代C内存管理手动管理new和delete虽然教育意义重大但在实际项目中容易出错。现代CC11及以上提供了智能指针来自动管理对象的生命周期。我们可以用std::unique_ptr来改造Document类的内存管理让代码更安全。思路是将char*数组替换为std::unique_ptrchar[]的数组。但更直接、更符合现代C风格的做法是在类内部使用std::vectorstd::string。这完全避免了手动内存管理。但为了演示智能指针在类似原始指针场景下的应用我们假设仍需管理一个char*数组。我们可以创建一个LinesContainer类来封装这个数组并使用std::unique_ptrchar*[]来管理指针数组本身用std::unique_ptrchar[]管理每个字符串。但这会使得代码复杂。一个更清晰的演示是如果我们Document类内部有另一个需要动态管理的资源对象比如一个负责缓存的Cache类我们可以用std::unique_ptrCache来管理它。#include memory class Cache { /* ... */ }; class ModernDocument { private: std::unique_ptrchar*[] lines; // 管理指针数组 // 或者更优std::vectorstd::unique_ptrchar[] linesVector; std::unique_ptrCache documentCache; // 使用智能指针管理成员对象 int lineCount; int capacity; public: ModernDocument(int cap 10) : capacity(cap), lineCount(0) { lines std::make_uniquechar*[](capacity); // 分配并初始化数组 for (int i 0; i capacity; i) lines[i] nullptr; documentCache std::make_uniqueCache(); // 自动构造Cache } // 不再需要显式定义析构函数unique_ptr会在ModernDocument销毁时自动释放它管理的资源。 // ~ModernDocument() {} // 可以省略 // 编译器生成的拷贝构造和赋值会被删除因为unique_ptr不可拷贝。 // 如果需要拷贝语义可以考虑使用shared_ptr或自己实现深拷贝。 };智能指针的核心价值std::unique_ptr确保了资源所有权的唯一性。当ModernDocument对象被销毁时它的成员lines和documentCache也会被销毁进而自动调用其析构函数释放内存。这几乎完全消除了内存泄漏的可能性。对于Document类内部的字符串数组虽然每个char*仍需手动delete[]但指针数组本身的管理被简化了。在实际项目中应优先考虑使用std::vectorstd::string。6. 编译、测试与常见问题排查6.1 项目编译与构建一个简单的多文件C项目可以使用命令行工具如g进行编译链接。# 在项目根目录下编译所有.cpp文件并指定输出可执行文件名为editor g -stdc11 -Wall -Wextra -g main.cpp document.cpp -o editor # 如果使用了继承/多态的示例还需要编译command相关的文件 g -stdc11 -Wall -Wextra -g main.cpp document.cpp insert_command.cpp delete_command.cpp -o editor_with_undo-stdc11指定使用C11标准确保智能指针等特性可用。-Wall -Wextra开启大部分警告信息帮助发现潜在问题。-g在可执行文件中添加调试信息便于使用GDB等工具调试。-o editor指定输出的可执行文件名。对于更复杂的项目强烈建议使用构建系统如CMake。创建一个CMakeLists.txt文件cmake_minimum_required(VERSION 3.10) project(LineEditor) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(editor main.cpp document.cpp) # 如果有多态示例 add_executable(editor_with_undo main.cpp document.cpp insert_command.cpp delete_command.cpp)然后在项目目录中执行mkdir build cd build cmake .. make6.2 典型问题排查实录在实现和测试这个编辑器的过程中我遇到了不少典型问题这里记录下排查思路问题1程序在插入或删除几次后崩溃Segmentation Fault可能原因1数组越界访问。在insertLine或deleteLine中访问lines[i]时i可能超出了[0, capacity-1]或[0, lineCount-1]的范围。排查在insertLine和deleteLine函数开头严格检查lineNumber参数。确保shiftLinesDown和shiftLinesUp中的循环索引正确。使用调试器GDB运行程序在崩溃时查看调用栈和变量值。可能原因2使用了已释放的内存悬空指针。在deleteLine中我们delete[] lines[lineNumber]后如果没有将lines[lineCount-1]设为nullptr并且在后续操作中错误地访问了这个位置比如在ensureCapacity中拷贝时就可能出错。排查确保在deleteLine中完成指针移动后将原最后一项置为nullptr。在析构函数中释放内存前检查指针是否为nullptrdelete[] nullptr是安全的。问题2内存泄漏程序运行越久占用内存越多可能原因分配的内存没有正确释放。最可能发生在insertLine失败如行号非法提前返回时可能已经为文本分配了内存但忘记释放。或者拷贝构造函数/赋值运算符实现有误导致资源重复释放或泄漏。排查使用内存检测工具如ValgrindLinux/Mac或Visual Studio的诊断工具Windows。用Valgrind运行程序valgrind --leak-checkfull ./editor它会详细报告内存泄漏的位置。预防遵循RAII原则。在函数中如果有可能在多个地方返回要确保所有分配的资源在返回前都被释放。或者更优的做法是使用智能指针或容器让资源管理自动化。问题3拷贝一个文档对象后操作其中一个会影响另一个原因浅拷贝问题。如果没有自定义拷贝构造函数和赋值运算符编译器会生成默认的它们只是简单地复制指针的值。这导致两个对象共享同一块内存。解决按照“三法则”为管理动态资源的类实现深拷贝构造函数和深拷贝赋值运算符或者使用“拷贝-交换”惯用法。问题4搜索功能对大小写敏感且找不到跨单词的匹配原因我们使用了C库函数strstr它进行的是大小写敏感的、精确的子串匹配。改进思路大小写不敏感搜索可以将字符串统一转换为小写或大写后再比较。使用std::transform和::tolower函数。单词边界匹配strstr会匹配任何位置的子串。如果想匹配完整单词需要更复杂的逻辑比如使用正则表达式C11的regex库或手动检查匹配位置前后的字符是否为非字母数字。使用C字符串如果内部改用std::vectorstd::string搜索可以使用std::string::find并且可以结合std::search和自定义比较器来实现更灵活的匹配。这个简单的行编辑器项目就像一块敲门砖敲开了C面向对象和系统编程的大门。从最基础的指针操作、内存分配到类的封装、继承多态再到现代C的智能指针每一步都对应着理解这门语言的一个关键层面。我个人的体会是不要畏惧手动管理内存的复杂性亲自踩过这些坑才能真正领悟RAII和智能指针为何如此重要。当你再看到std::vector或std::string时你就能清晰地想象出它们背后是如何工作的这能让你写出更高效、更安全的代码。最后分享一个小技巧在实现这类数据结构时画图和写单元测试是最高效的调试手段。在纸上画出数组、指针在每一步操作后的状态变化能帮你理清逻辑避免差一错误。而为每个核心函数如insertLine,deleteLine, 拷贝构造编写简单的测试用例则能快速验证其正确性并在后续修改时防止回归错误。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2595177.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…