首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++/Qt 项目:把"能编译"做成"能长期维护"

C++/Qt 项目:把"能编译"做成"能长期维护"

原创
作者头像
资源shanxueit.com
发布于 2026-10-31 15:22:34
发布于 2026-10-31 15:22:34
1790
举报

二、先做两个选型决策

在写第一行代码之前,有两个决策会影响整个项目的走向,且后期极难更改。

第一,Widgets 还是 QML。

维度

QWidget

QML/Qt Quick

适合场景

桌面工具、表单密集、传统业务系统

触屏、动效丰富、嵌入式 HMI

语言

纯 C++

QML(声明式) + C++ 后端

复杂表格/树

成熟稳定

需额外投入

动画与自绘

需手写 paintEvent

天然支持

团队门槛

C++ 即可

需额外学 QML

判断标准很直接:界面以"数据录入与展示"为主,选 Widgets;以"交互体验与动效"为主,选 QML。 不要因为 QML"看起来现代"就盲目选它——一个表单密集的管理系统用 QML 写,工作量可能是 Widgets 的两倍。

第二,构建系统选 CMake。

qmake 已进入维护状态,新项目没有理由再用。CMake 是 Qt 6 的官方推荐,与生态工具链(Conan、vcpkg、CI)集成更好。这个决策没有争议空间。


三、分层:把 UI 和业务彻底隔开

Qt 项目最常见的架构病是业务逻辑长在窗口类里。一个 MainWindow.cpp 动辄两千行,数据库查询、网络请求、界面刷新混在一起,既不能测,也不能复用。

合理的分层大致是四层:

  • UI 层:窗口、控件、QML 文件。只负责展示与收集输入,不含业务判断。
  • ViewModel / Controller 层:连接 UI 与业务,做数据转换、状态管理、命令分发。
  • Service / Domain 层:纯 C++ 的业务逻辑,不依赖 Qt GUI 模块,可独立单元测试。
  • Infrastructure 层:数据库、网络、文件、设备驱动等外部依赖的具体实现。

关键约束只有一条:Domain 层不允许 include 任何 QtWidgets 或 QtQuick 头文件。 这条约束一旦守住,业务逻辑就能脱离界面单独测试,这是 Qt 项目可维护性的分水岭。

依赖方向应当单向向下,上层通过接口依赖下层,而不是直接 new 具体实现。下面是一个最小的接口骨架:

代码语言:javascript
复制
// domain/order_service.h —— 纯 C++,不依赖 GUI
class OrderService {
public:
    virtual ~OrderService() = default;
    virtual Result<void> submit(const Order& order) = 0;
    virtual QVector<Order> query(const Filter& filter) = 0;
};

// ui/order_page.cpp —— UI 只依赖接口
class OrderPage : public QWidget {
    Q_OBJECT
public:
    explicit OrderPage(OrderService* svc, QWidget* parent = nullptr)
        : QWidget(parent), m_svc(svc) {}
private slots:
    void onSubmitClicked();
private:
    OrderService* m_svc;   // 不拥有,由上层注入
};

这段代码的重点不在写法,而在依赖注入的方向:UI 持有接口指针,不关心背后是数据库还是 mock。测试时传一个假实现即可,不需要启动界面。


四、对象生命周期:Qt 最大的坑区

Qt 有一套独特的对象所有权模型:QObject 的父子关系会在父对象析构时自动删除子对象。 这套机制很方便,但和 C++ 的 RAII、智能指针混用时极易出事。

几条必须守住的纪律:

第一,明确每个对象的归属。 要么交给 Qt 父子树管理(new QWidget(this)),要么交给智能指针管理(std::unique_ptr),不要两种混用。一个对象既挂了 parent 又被 unique_ptr 持有,必然双重释放。

第二,栈对象不要设 parent。 QWidget w; w.setParent(p); 是典型的崩溃源头——栈对象析构时 Qt 已经把它从父子树里删了一次。

第三,跨线程传递对象要小心。 QObject 有线程亲和性,一个对象只能在它所属的线程被操作。跨线程通信用信号槽的队列连接,而不是直接调用方法。

第四,析构顺序很重要。 如果 A 在析构时还要访问 B,那 B 必须比 A 活得久。用父子关系时,Qt 的删除顺序是未定义的,不能依赖。

一个实用建议是:项目里统一约定所有权规则并写进规范,比如"所有 QObject 派生类一律用父子树管理,非 QObject 资源一律用 RAII 包装"。规则统一,崩溃就少一大半。


五、信号槽:好用,但要有边界

信号槽是 Qt 的灵魂,也是最容易被滥用的地方。常见病症:

  • 信号链路过长:A 发信号给 B,B 转发给 C,C 再转发给 D,出问题时根本追不到源头。
  • 用信号槽做同步调用:明明可以直接返回值,却绕一圈发信号,增加复杂度。
  • 连接数量失控:同一个信号连了十几个槽,顺序和副作用都不可控。

几条实践原则:

连接类型要明确。 Qt::AutoConnection 在跨线程时会自动变成队列连接,但依赖"自动"容易埋雷。跨线程场景显式写 Qt::QueuedConnection,意图更清楚。

连接要可断开、可管理。 用 QMetaObject::Connection 保存连接句柄,在对象销毁或状态切换时主动断开,而不是依赖自动断开。

优先用新式语法。 函数指针式连接(connect(a, &A::sig, b, &B::slot))编译期就能检查签名,比字符串式 SIGNAL/SLOT 宏安全得多。

信号只承载"发生了什么事",不承载业务逻辑。 槽函数里做业务,信号本身保持轻量。

一段典型的跨线程骨架:

代码语言:javascript
复制
// 工作对象在子线程,结果通过队列连接回主线程
Worker* worker = new Worker;              // 无 parent,稍后 moveToThread
QThread* thread = new QThread(this);      // 由 this 管理生命周期
worker->moveToThread(thread);

connect(thread, &QThread::started, worker, &Worker::run);
connect(worker, &Worker::finished, this, &Controller::onFinished,
        Qt::QueuedConnection);            // 显式跨线程
connect(worker, &Worker::finished, thread, &QThread::quit);
connect(thread, &QThread::finished, worker, &QObject::deleteLater);

thread->start();

这段代码里几个细节值得注意:worker 不能有 parent(否则无法 moveToThread)、用 deleteLater 而非 delete(在正确的线程析构)、线程对象由 this 管理(避免泄漏)。


六、跨平台构建:让"换台机器编不过"成为历史

Qt 的跨平台是卖点,但工程上要真正做到"一次编写、处处构建",需要一些纪律。

用 CMake 统一构建。 一份 CMakeLists.txt 覆盖 Windows/macOS/Linux,平台差异用生成器表达式或条件分支处理,而不是维护多份工程文件。

依赖管理要显式。 Qt 6 的 find_package(Qt6 REQUIRED COMPONENTS Widgets Network Sql) 明确列出用到的模块,避免隐式依赖。第三方库建议用 Conan 或 vcpkg 管理,锁版本、可复现。

平台相关代码集中隔离。 文件路径、注册表、系统托盘、原生句柄这些差异,统一放进 platform/ 目录,用编译宏或接口隔离,业务代码里不出现 #ifdef Q_OS_WIN。

CI 覆盖所有目标平台。 至少做到三个平台都能编译通过、核心单元测试通过。很多"跨平台"项目实际只在开发者自己的机器上验证过,一旦进 CI 就暴露。

一个最小的 CMake 骨架:

代码语言:javascript
复制
cmake_minimum_required(VERSION 3.21)
project(myapp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)      # 元对象编译器
set(CMAKE_AUTORCC ON)      # 资源文件

find_package(Qt6 REQUIRED COMPONENTS Widgets Network Sql)

add_library(domain STATIC src/domain/order_service.cpp)   # 无 Qt GUI 依赖
target_link_libraries(domain PRIVATE Qt6::Core)           # 仅需 Core

qt_add_executable(myapp src/main.cpp src/ui/order_page.cpp)
target_link_libraries(myapp PRIVATE domain Qt6::Widgets Qt6::Network Qt6::Sql)

注意 domain 库只链接 Qt6::Core——这是分层约束在构建系统里的落地。编译期就能发现越界依赖,比代码评审更可靠。


七、资源、国际化与配置

资源文件。 图标、样式表、QML 用 .qrc 打进二进制,避免运行时找文件路径。但要注意:大资源会撑大可执行文件,且修改资源需要重新编译。频繁变动的资源(如可换肤的图片)应放外部目录。

国际化。 从一开始就用 tr() 包住所有用户可见文本,哪怕暂时只有一种语言。后期补 tr() 的成本远高于一开始就写。用 lupdate/lrelease 生成 .ts/.qm,切换语言时动态加载 QTranslator。

配置管理。 QSettings 适合存用户偏好,但不要用它存业务配置。业务配置应该有明确的 schema、版本号和校验,放在独立配置文件里。


八、性能:Qt 项目的几个高频瓶颈

  • 界面卡顿:主线程做耗时操作是头号原因。数据库查询、文件扫描、网络请求一律移到工作线程,主线程只负责刷新。
  • 大量数据展示:QTableWidget 适合小数据量,上万行必须换 QTableView + QAbstractTableModel,只渲染可见区域。
  • 频繁重绘:避免在 paintEvent 里做复杂计算;用 update() 而非 repaint();合理设置 WA_OpaquePaintEvent 等属性。
  • 信号风暴:批量更新时先 blockSignals(true) 或用 QSignalBlocker,更新完再恢复。
  • 内存增长:QObject 父子树管理不当、连接未断开、缓存无上限,都是常见泄漏源。用 Valgrind 或 ASan 定期检查。

性能优化的前提是先测量再动手。Qt 自带 QElapsedTimer,配合各平台的 profiler(perf、Instruments、VTune),定位到真正的热点再优化,而不是凭感觉改代码。


九、测试:让重构有底气

Qt 项目的测试分三层:

  • 单元测试:Domain 层纯 C++ 逻辑,用 Qt Test 或 Catch2/GoogleTest,不启动界面。覆盖率重点放在这一层。
  • 组件测试:单独测试某个 Widget 或 Model 的行为,可用 QTest::mouseClick 等模拟交互。
  • 集成测试:启动完整应用,走关键流程。数量少、价值高,适合用 Squish 或自研脚本。

关键是Domain 层要能被测——这也是前面坚持分层隔离的原因。如果业务逻辑全在窗口类里,测试就只能靠手动点点点,重构时没人敢动代码。


十、发布与部署

Qt 应用的部署有几个固定动作:

  • Windows:用 windeployqt 收集依赖 DLL,注意 MSVC 运行时和平台插件目录。
  • macOS:用 macdeployqt 生成 .app,签名与公证(notarization)是上架 App Store 或分发的必要步骤。
  • Linux:AppImage 或 Flatpak 是较省心的分发方式,注意 Qt 平台插件(xcb/wayland)的依赖。

版本号与更新机制要提前设计。 至少要有明确的版本号规则、升级包格式、回滚方案。桌面应用的自动更新比 Web 复杂得多,早规划省事。

崩溃收集。 接入 Breakpad/Crashpad 或平台自带方案,收集堆栈。Qt 应用的崩溃很多发生在事件循环里,没有堆栈几乎无法定位。


十一、常见陷阱

  • 业务逻辑写进窗口类:无法测试、无法复用,重构时寸步难行。
  • 父子树与智能指针混用:双重释放,崩溃在析构时爆发。
  • 栈对象设 parent:典型崩溃源头。
  • 主线程做耗时操作:界面假死,用户以为程序挂了。
  • 信号链路过长:出问题追不到源头。
  • 平台代码散落各处:#ifdef 满天飞,跨平台成了笑话。
  • 不用 tr():后期国际化成本翻倍。
  • 大数据用 QTableWidget:上万行直接卡死。
  • 不做 CI 跨平台验证:在自己机器上能编,别人机器上编不过。
  • 不接崩溃收集:线上崩溃只能靠用户描述复现。

十二、小结

C++/Qt 项目的工程方法论可以浓缩成四句话:分层隔离、所有权明确、构建统一、测试先行。

Qt 提供了很好的基础设施——元对象、跨平台抽象、成熟的控件库。但这些只是原材料,能不能盖成能长期住的房子,取决于架构决策:UI 和业务分不分得开、对象归属清不清楚、跨平台是不是真的跨、重构时有没有测试兜底。

框架决定你能写多快,架构决定你能维护多久。 前者几个月就能学会,后者要靠一个个项目踩出来——而避开上面这些坑,能省掉其中大半。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 二、先做两个选型决策
  • 三、分层:把 UI 和业务彻底隔开
  • 四、对象生命周期:Qt 最大的坑区
  • 五、信号槽:好用,但要有边界
  • 六、跨平台构建:让"换台机器编不过"成为历史
  • 七、资源、国际化与配置
  • 八、性能:Qt 项目的几个高频瓶颈
  • 九、测试:让重构有底气
  • 十、发布与部署
  • 十一、常见陷阱
  • 十二、小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档