C++项目中MVC架构落地:分层设计、依赖解耦与工程实践
很多 C 开发者第一次接触 MVC印象往往是“这不是 Java Web 和前端框架里的概念吗跟 C 有什么关系”等到真正在 C 项目里动手做架构又会发现另一个问题代码写到最后界面逻辑、业务规则和数据结构全搅在一起改一个按钮要翻遍整个类测一个核心算法必须先启动图形界面。这篇文章想解决的问题很明确在 C 项目里MVC 到底应该怎么落地。它不是一个只能在 Web 框架里生效的花瓶理论而是一套能让你把“界面代码”“业务规则”“数据存取”清楚拆开的分层控制方法。读完你会明白 MVC 三个角色在 C 里分别对应哪些类、哪些文件、哪些依赖关系并且能在不引入重型框架的情况下用最朴素的 C 语法写一个可运行、可扩展、可测试的最小 MVC 示例。先给一个判断MVC 在 C 项目里真正的价值不是“看起来结构清晰”而是让依赖方向变得可控。只要做到 Model 不知道 View 的存在View 不直接写数据库Controller 不堆积业务逻辑你的项目就具备了后续重构、写单测、换界面框架的余地和空间。下面从概念开始一步一步落到代码上。1. C 开发中为什么也需要 MVC先看一个非常常见的 C 小项目演进过程。假设你写了一个学生成绩管理程序一开始很自然的做法是在窗口类里放一个表格控件再放几个按钮点击“加载”按钮就打开数据库把数据塞进表格点击“统计”按钮就写一段循环计算平均分再弹个对话框显示结果。这个阶段代码量小一切正常。但当功能慢慢变多比如加上了导出 Excel、成绩排名、按班级筛选、批量修改、操作日志记录窗口类的成员函数开始膨胀到两三千行。此时任何一次界面调整比如把表格从第三方控件换成自绘控件都会牵扯到数据访问代码任何一次数据库表结构变更又需要在界面类里找哪些地方用了旧字段。时间一长这个项目就进入“很难改、不敢改、改一处崩三处”的状态。这正是 MVC 要解决的问题。它的核心思路不是减少代码量而是改变代码之间的依赖方向界面层View只负责显示和收集输入它不关心数据从哪里来、如何计算。控制层Controller接收用户操作调用业务逻辑再让界面更新但控制层不应该写复杂的业务算法。模型层Model保存数据、实现业务规则它独立于界面存在可以被单独测试和复用。过去几年里后端开发领域大量讨论分布式架构、微服务架构其实背后的动机和 MVC 是相通的把容易变化的部分隔离起来让整个系统可以被理解、被修改、被测试。C 虽然常年给人“性能优先、架构随意”的印象但一个 C 项目一旦进入长期维护阶段分层设计带来的收益会远超那一点抽象成本。一句话总结本节C 需要 MVC不是因为 C 开发者比别的语言开发者更讲究而是因为只要代码规模变大依赖混乱的成本就会指数级上升MVC 是最简单也最通用的应对手段之一。2. MVC 核心概念与 C 中的对应关系很多人一提 MVC 就背口诀“Model 数据、View 显示、Controller 控制”但真正写代码时依然不知道类该怎么划分。这里需要把概念重新翻译成 C 开发语言。2.1 模型Model与界面无关的数据与规则在 C 里Model 通常是普通类或结构体负责保存业务数据和实现业务规则。它不应该 include 任何界面头文件不应该使用 Windows API、Qt 的 QWidget、MFC 的 CWnd 等界面相关类型。Model 的基本构成包括成员变量如学生姓名、学号、成绩。业务方法如计算平均分、判断是否及格、排序。数据访问方法如从文件或数据库加载、保存。举一个类比Model 就像餐厅后厨的菜品数据库和菜谱。厨师长只关心食材数据和处理工艺他不知道也不需要知道这道菜会被放在什么样的盘子里端上桌。2.2 视图View负责展示与输入收集View 负责把数据呈现给用户并收集用户操作。在 C 桌面开发中View 就是对话框、窗口、控件、自绘区域在 C 命令行工具中View 就是标准输出和标准输入。设计原则是View 只负责“长得好看”和“把事件传出去”不应该自己去数据库查数据也不应该写“如果成绩大于 60 分则……”这种业务判断。业务判断应该交给 Model 或 Controller。有些 C 项目会把 View 写得非常薄甚至只有界面描述文件比如 Qt 的.ui文件。这是一种很好的趋势。View 越薄迁移成本越低。2.3 控制器Controller连接用户操作与业务逻辑Controller 接收 View 转发的用户操作调用 Model 的方法再请 View 刷新显示。它有点像餐厅的前厅经理顾客用户向服务员View点菜服务员把菜单交给经理Controller经理向后厨Model下单后厨做完后经理通知服务员上菜。Controller 应该很“瘦”不应该堆积具体业务算法。如果一个 Controller 成员函数超过几十行并且主要在做算术、统计、字符串解析那这些逻辑很可能应该下沉到 Model 中。2.4 容易混淆的对比C MVC 与 Web MVC、三层架构由于网上大量资料来自 Java、C# 的 Web MVC很多 C 开发者会产生两个误区。误区一把 MVC 等同于三层架构。实际上 MVC 是表现层的内部划分三层架构中的“业务逻辑层”对应到 MVC 中通常被放在 Model 里或再拆出 Service 层两者不是同一个维度的概念。误区二认为 View 是“网页模板”所以 C 没网页就不需要 MVC。实际上 C 里的 View 就是窗口、控件、命令行打印MVC 思想完全适用。下面用表格对比一下概念Web MVCJava 示例C 桌面/服务端 MVCModelEntity、Service、Mapper业务数据类、算法类、数据存取类ViewJSP / Thymeleaf 模板Qt 窗口、MFC 对话框、控制台打印ControllerSpring MVC 的 Controller接收 UI 事件的类数据流HTTP 请求 → Controller → Model → View控件事件 → Controller → Model → 刷新 View依赖方向高层依赖抽象不反向依赖Model 不依赖 ViewView 不写业务这个对比的价值在于理解 MVC 不能背框架而是要看清楚“职责边界”和“依赖方向”这两个更底层的东西。3. C MVC 依赖方向与分层边界有了概念框架下一步就是明确具体的依赖边界否则还是会写成“把数据类往上提一层、把界面类往下塞一层”的表面 MVC。3.1 依赖方向的硬性规则在一个合格的 C MVC 项目里依赖边界应该符合以下几点Model 不依赖 View也不依赖具体窗口框架。View 可以知道 Model 的存在为了显示数据但不应反向修改 Model 的业务状态除非通过 Controller。Controller 需要同时知道 View 和 Model可以把二者连接起来。数据流是单向的View 发出用户请求 → Controller 处理请求 → Model 更新数据 → View 重新获取数据并刷新。如果违反规则出现 Model 里 include 了 QLabel或者 View 里直接写 SQL那么架构就已经开始腐化了。3.2 如何防止 View 反向依赖 ModelC 里最常用的手段是回调、信号槽或观察者模式。Qt 里用信号槽是最自然的做法。不依赖 Qt 的纯 C 项目可以使用std::function回调把“数据变化后要通知谁”这件事通过 Controller 注入到 Model 中。这里给出一个简单的伪代码级别的感受// 模型只提供一个回调注册接口不关心通知给谁 void StudentManager::setOnDataChanged(std::functionvoid() callback) { m_onDataChanged callback; } void StudentManager::addStudent(const Student s) { m_students.push_back(s); if (m_onDataChanged) { m_onDataChanged(); // 通知 Controller 去刷新 View } }通过这种方式Model 不需要 include 任何 View 头文件却能实现“数据变化后界面自动刷新”的效果。这是 C MVC 架构里非常关键的一种解耦技巧。3.3 一个判断依赖方向的简单方法每次写完一个类可以做一个十分钟的检查看这个类的头文件 include 列表如果出现了 GUI 框架的头文件而该类自称是 Model那就有问题。看这个类的成员函数如果出现了“把数据直接显示到某个控件”的代码那么它更像是 View 或 View 的辅助代码不是 Model。看这个类是否知道“用户点击了哪个按钮”如果知道那么它至少是 Controller不应承担数据存取职责。这种检查不需要工具靠肉眼和代码审查就能完成。架构最终是在日常提交中维持的不是一次设计就一劳永逸。4. 环境准备与项目结构设计接下来用一个可运行的 C 示例把 MVC 三个角色落到真实代码里。这个例子是“学生成绩管理”的最小版本功能包括添加学生、按学号查找、计算全班平均分。为了让示例在任何环境都能编译运行这里不依赖 Qt、MFC 等界面库而是用控制台程序来演示。控制台同样可以扮演 View 的角色好处是逻辑清晰读者可以专注看分层不被控件细节干扰。4.1 环境说明编译器支持 C11 及以上的编译器即可如 GCC、Clang、MSVC。构建工具本文使用 CMake 示例也可以用单文件编译。操作系统Windows / Linux / macOS 均可代码不涉及平台特有 API。版本细节以实际环境为准本文重点是演示通用思路不绑定特定版本。建议目录结构如下student_mvc/ ├── CMakeLists.txt ├── src/ │ ├── model/ │ │ ├── Student.h │ │ ├── Student.cpp │ │ ├── StudentManager.h │ │ └── StudentManager.cpp │ ├── view/ │ │ └── ConsoleView.h │ ├── controller/ │ │ └── StudentController.h │ └── main.cpp在实际项目中Model 目录下还可以继续划分为entity、repository、serviceController 下也可以拆出多个控制器。但是不要一开始就设计过度。最小可用的分层比一张漂亮的 UML 图更有价值。4.2 CMakeList 示例cmake_minimum_required(VERSION 3.10) project(StudentMvcDemo) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(student_mvc src/model/Student.cpp src/model/StudentManager.cpp src/view/ConsoleView.cpp src/controller/StudentController.cpp src/main.cpp )如果使用 Qt则建议采用find_package(Qt5 COMPONENTS Widgets)并开启元对象编译器。但本文示例不用 Qt 也能跑通。5. 完整示例代码实现现在进入最核心的部分。下面的代码分为四组文件先来看 Model 层。5.1 Model 层Student 与 StudentManager先定义学生实体类。文件路径src/model/Student.h#ifndef STUDENT_H #define STUDENT_H #include string class Student { public: Student(); Student(const std::string id, const std::string name, double score); std::string getId() const; std::string getName() const; double getScore() const; bool isPass() const; // 业务规则是否及格 private: std::string m_id; std::string m_name; double m_score; }; #endif文件路径src/model/Student.cpp#include Student.h Student::Student() : m_score(0.0) { } Student::Student(const std::string id, const std::string name, double score) : m_id(id), m_name(name), m_score(score) { } std::string Student::getId() const { return m_id; } std::string Student::getName() const { return m_name; } double Student::getScore() const { return m_score; } bool Student::isPass() const { return m_score 60.0; }这里要强调一个细节isPass()是业务规则放在 Model 中是合理的。View 在显示及格状态时只需要调用isPass()不需要自己写score 60的判断。业务规则一旦变化比如改成 65 分及格只需修改 Model 一处界面不用动。再定义负责管理学生集合的类。文件路径src/model/StudentManager.h#ifndef STUDENT_MANAGER_H #define STUDENT_MANAGER_H #include string #include vector #include functional #include Student.h class StudentManager { public: using OnDataChanged std::functionvoid(); void setOnDataChanged(OnDataChanged callback); bool addStudent(const Student student); bool removeStudent(const std::string id); const Student* findStudent(const std::string id) const; std::vectorStudent getAllStudents() const; double getAverageScore() const; private: std::vectorStudent m_students; OnDataChanged m_onDataChanged; }; #endif文件路径src/model/StudentManager.cpp#include StudentManager.h #include algorithm void StudentManager::setOnDataChanged(OnDataChanged callback) { m_onDataChanged callback; } bool StudentManager::addStudent(const Student student) { // 学号不允许重复 for (const auto s : m_students) { if (s.getId() student.getId()) { return false; } } m_students.push_back(student); if (m_onDataChanged) { m_onDataChanged(); } return true; } bool StudentManager::removeStudent(const std::string id) { auto it std::remove_if(m_students.begin(), m_students.end(), [id](const Student s) { return s.getId() id; }); if (it m_students.end()) { return false; } m_students.erase(it, m_students.end()); if (m_onDataChanged) { m_onDataChanged(); } return true; } const Student* StudentManager::findStudent(const std::string id) const { for (const auto s : m_students) { if (s.getId() id) { return s; } } return nullptr; } std::vectorStudent StudentManager::getAllStudents() const { return m_students; } double StudentManager::getAverageScore() const { if (m_students.empty()) { return 0.0; } double sum 0.0; for (const auto s : m_students) { sum s.getScore(); } return sum / m_students.size(); }这段代码里最关键的是setOnDataChanged回调。Model 在数据变化后不是直接去调某个窗口刷新而是发出通知由 Controller 决定如何处理。设计上的收益是将来把控制台 View 换成 Qt ViewModel 代码完全不需要修改。5.2 View 层控制台视图文件路径src/view/ConsoleView.h#ifndef CONSOLE_VIEW_H #define CONSOLE_VIEW_H #include string #include vector #include model/Student.h class ConsoleView { public: void showMenu() const; int readChoice() const; Student readNewStudent() const; std::string readStudentId() const; void showStudent(const Student* student) const; void showAllStudents(const std::vectorStudent students) const; void showAverage(double average) const; void showMessage(const std::string message) const; }; #endif文件路径src/view/ConsoleView.cpp#include ConsoleView.h #include iostream #include iomanip void ConsoleView::showMenu() const { std::cout \n Student Management System \n; std::cout 1. Add Student\n; std::cout 2. Remove Student\n; std::cout 3. Find Student\n; std::cout 4. Show All Students\n; std::cout 5. Show Average Score\n; std::cout 0. Exit\n; std::cout Your choice: ; } int ConsoleView::readChoice() const { int choice 0; std::cin choice; return choice; } Student ConsoleView::readNewStudent() const { std::string id; std::string name; double score 0.0; std::cout Enter student id: ; std::cin id; std::cout Enter student name: ; std::cin name; std::cout Enter student score: ; std::cin score; return Student(id, name, score); } std::string ConsoleView::readStudentId() const { std::string id; std::cout Enter student id: ; std::cin id; return id; } void ConsoleView::showStudent(const Student* student) const { if (student nullptr) { std::cout Student not found.\n; return; } std::cout ID: student-getId() , Name: student-getName() , Score: student-getScore() , Status: (student-isPass() ? Pass : Fail) \n; } void ConsoleView::showAllStudents(const std::vectorStudent students) const { if (students.empty()) { std::cout No students.\n; return; } for (const auto s : students) { showStudent(s); } } void ConsoleView::showAverage(double average) const { std::cout Average score: std::fixed std::setprecision(2) average \n; } void ConsoleView::showMessage(const std::string message) const { std::cout message \n; }View 层只做三件事打印菜单、读取用户输入、格式化显示数据。注意showStudent()里调用了student-isPass()这是合理的因为isPass()是业务规则暴露出来的只读接口View 只需要消费规则结果不需要理解规则内部逻辑。5.3 Controller 层连接输入与数据文件路径src/controller/StudentController.h#ifndef STUDENT_CONTROLLER_H #define STUDENT_CONTROLLER_H #include model/StudentManager.h #include view/ConsoleView.h class StudentController { public: StudentController(StudentManager model, ConsoleView view); void run(); private: bool handleChoice(int choice); StudentManager m_model; ConsoleView m_view; }; #endif文件路径src/controller/StudentController.cpp#include StudentController.h StudentController::StudentController(StudentManager model, ConsoleView view) : m_model(model), m_view(view) { } void StudentController::run() { bool running true; while (running) { m_view.showMenu(); int choice m_view.readChoice(); if (choice 0) { running false; } else { running handleChoice(choice); } } } bool StudentController::handleChoice(int choice) { switch (choice) { case 1: { Student newStudent m_view.readNewStudent(); bool ok m_model.addStudent(newStudent); if (ok) { m_view.showMessage(Add student success.); } else { m_view.showMessage(Add failed. Duplicate id.); } return true; } case 2: { std::string id m_view.readStudentId(); bool ok m_model.removeStudent(id); m_view.showMessage(ok ? Remove success. : Remove failed. Id not found.); return true; } case 3: { std::string id m_view.readStudentId(); const Student* stu m_model.findStudent(id); m_view.showStudent(stu); return true; } case 4: { m_view.showAllStudents(m_model.getAllStudents()); return true; } case 5: { m_view.showAverage(m_model.getAverageScore()); return true; } default: m_view.showMessage(Invalid choice. Please try again.); return true; } }Controller 的每个分支都非常短读取输入调用 Model显示结果。这就是“瘦控制器”的体现。如果某个分支开始变得复杂比如添加学生时需要校验身份证号、计算年龄、检查学分那这些逻辑应该放进 Model 或独立的 Service 类而不是全部堆在 Controller 里。5.4 入口程序组装三者文件路径src/main.cpp#include model/StudentManager.h #include view/ConsoleView.h #include controller/StudentController.h int main() { StudentManager model; ConsoleView view; // 数据变化后自动刷新界面 model.setOnDataChanged([view]() { view.showMessage(Data changed. You can refresh the view here.); }); StudentController controller(model, view); controller.run(); return 0; }main 函数只做依赖组装。运行顺序是创建 Model 和 View。通过回调把数据变化通知关联好。创建 Controller并把 Model 和 View 注入进去。启动 Controller 的交互循环。如果以后换成 Qt 版本main 函数会换成 QApplication 加窗口对象的组装但 Model 与 Controller 的核心代码可以保留这就是分层带来的模块复用能力。5.5 如何编译运行在项目根目录执行mkdir build cd build cmake .. make ./student_mvcWindows 下可以用 Visual Studio 打开 CMake 项目或者直接使用cmake -S . -B build cmake --build build --config Release build\Release\student_mvc.exe6. 运行结果与效果验证程序运行后会看到类似下面的输出 Student Management System 1. Add Student 2. Remove Student 3. Find Student 4. Show All Students 5. Show Average Score 0. Exit Your choice: 1 Enter student id: 001 Enter student name: Alice Enter student score: 88 Add student success. Data changed. You can refresh the view here.继续添加第二个学生然后查看全部学生和平均分Your choice: 4 ID: 001, Name: Alice, Score: 88.00, Status: Pass ID: 002, Name: Bob, Score: 59.00, Status: Fail Your choice: 5 Average score: 73.50验证架构是否真正生效不是看功能是否跑通而是做三个检查检查 Model 代码中是否完全找不到iostream或任何窗口相关头文件。正常情况是 Model 只有数据操作和回调不执行打印。检查 View 代码中是否不直接操作m_students的私有数据。正常情况是 View 通过公共接口读取数据。检查 Controller 是否只是“传话筒”。如果 Controller 里出现了复杂计算逻辑那就说明 Model 的职责被抽空了。这三个检查比任何理论都更能说明 MVC 是否落地。7. 常见问题与排查思路实际在 C 项目中使用 MVC 时最常见的不是概念不理解而是细节踩坑。下面整理几张排查表。问题现象可能原因排查方式解决方案Model 文件中出现了 UI 相关头文件职责划分混乱Model 被写入了界面依赖检查 include 列表把界面相关代码移到 ViewModel 改为通过回调暴露数据变化编译报错字段类型不完整无法使用头文件互相包含或 forward declaration 使用不当查看编译器错误位置理清头文件依赖前置声明能解决大部分循环包含数据变化后界面不会自动刷新回调未注册或 Controller 未调用 setOnDataChanged在回调函数中加日志在 Model 构造函数中提供默认空回调并在调用处显式注册Controller 代码越来越膨胀业务规则被放到了 Controller 中统计每个函数行数把算法抽到 Model 或独立 Service 层Controller 保持瘦身重构 Model 时导致 View 编译失败View 依赖了 Model 的私有字段或直接调用内部方法检查 View 对 Model 的访问View 只使用公共只读接口私有字段一律不暴露多线程操作数据时崩溃Model 没有做线程同步查看崩溃调用栈在 Model 内部加锁或限流 Controller 的并发访问这里面最容易复发的其实是第一个问题。很多 C 项目一开始分层很好后来为了“快速加一个功能”直接在 Model 里塞了一个 UI 对象引用几周后架构就名存实亡。建议在代码评审中把“Model 禁止依赖 UI 头文件”作为硬性规则。还有一个常见问题回调导致的生命周期问题。如果 View 或 Controller 在 Model 销毁前已经被销毁回调变成悬空引用程序会在数据变化时崩溃。解决方法是使用弱引用或确保所有者的析构顺序正确。在 Qt 中信号槽机制已经处理了大部分生命周期问题但在手写回调的代码中必须非常小心。8. 最佳实践与工程建议8.1 使用接口类解耦 View如果项目界面技术可能在未来发生替换比如从 Win32 换到 Qt或从 MFC 换到自绘框架建议给 View 定义一个抽象接口。Controller 持有接口指针而不是持有具体类型。class IStudentView { public: virtual ~IStudentView() default; virtual void showMenu() 0; virtual int readChoice() 0; virtual void showStudent(const Student* student) 0; virtual void showAllStudents(const std::vectorStudent students) 0; virtual void showMessage(const std::string message) 0; };ConsoleView 继承并实现这个接口。未来要写 QtView 时只需要实现同一组接口Controller 完全不用改。这是 C 面向接口编程在 MVC 中的典型应用。8.2 Controller 不要直接依赖具体 Model 类型如果业务规模较大建议把 Model 的操作抽象为接口比如IStudentServiceController 持有IStudentService指针。这样单元测试时可以用 Mock 对象替换真实数据管理类测试 Controller 的交互逻辑不需要真实数据库或文件系统。8.3 Model 与 View 的数量关系一个复杂业务模块可以有多个 View 监听同一个 Model比如一个表格视图加一个统计视图两者同时显示同一份数据。数据变化时Model 通过回调或观察者列表通知所有 View。这正是 MVC 相对于“一个界面类包打天下”的重要优势。8.4 保持依赖注入避免全局单例很多 C 项目喜欢写全局单例 Manager然后所有类都直接调用StudentManager::instance()。这种写法在小型工具里很快但会让依赖关系不可见。更好的做法是在 main 函数或 App 启动区显式创建对象并通过构造函数或 setter 注入到 Controller、View 中。依赖关系一旦变成构造函数参数每个类的依赖就一目了然了。8.5 重视 Model 层的单元测试MVC 分层后最大的测试红利在 Model 层。因为 Model 不依赖界面可以直接写测试用例验证业务逻辑。下面是一个简单测试片段// 伪代码示例实际可用 Google Test 等框架 void testAverageScore() { StudentManager manager; manager.addStudent(Student(001, Alice, 80)); manager.addStudent(Student(002, Bob, 70)); assert(manager.getAverageScore() 75.0); }不用启动界面、不用数据库、不用模拟用户点击几十毫秒就能跑完。随着项目变大Model 层的测试覆盖可以成为项目最可靠的保护网。8.6 版本兼容与 C 标准选择老项目可能还停留在 C03这种情况下没有std::function和 lambda可以使用函数指针或观察者接口替代回调。C11 及以后的版本写 MVC 会舒服很多std::function、lambda、智能指针、std::vector的初始化列表都让代码更短更清晰。如果团队没有历史包袱建议统一使用 C14 或 C17并启用编译器的警告选项。8.7 避免 MVC 滥用不是所有 C 程序都需要完整 MVC。一个嵌入式设备里的状态采集程序一个快速原型工具一个单文件算法演示硬拆出 model/view/controller 三组类反而增加负担。判断标准是代码是否可能被复用到不同界面业务规则是否复杂到需要单独测试项目是否要长期维护多人协作如果答案都是否那就不要 MVC。9. 总结与后续学习方向本文围绕 C 开发中的 MVC 架构讲清楚了几个关键问题MVC 解决的是依赖混乱问题而不是代码量问题Model 不依赖 ViewView 不写业务规则Controller 负责调度回调机制是 C 里实现 Model 与 View 解耦的实用手段完整的控制台学生管理示例可以在不引入 GUI 框架的前提下跑通整个 MVC 流程。如果读者想继续深入建议按下面路径练习先用本文的控制台示例跑通做一次代码检查确认 Model、View、Controller 没有越界。尝试把 View 换成 Qt 窗口版本保留 Model 和 Controller观察哪些代码没动。给 Model 写入文件持久化能力比如 JSON 或 CSV继续维持 Model 不依赖界面。引入 Google Test 或 Catch2 对 Model 写单元测试。如果你已经接触过 Spring MVC 或 ASP.NET MVC可以对比 C 与 Web 框架中的 MVC 实现差异会更强烈地感受到“架构模式是语言无关的”。架构最终不是靠某个框架自动生成的而是靠每一次类设计、每一次函数拆分、每一次代码评审积累出来的。C 的 MVC 也一样核心约束就是几条关键看你能不能长期执行下去。建议把文章里的示例工程保存下来下一次写 C 小工具时直接用它作为初始骨架比新建一个什么都揉在一起的 main.cpp 要省心得多。