# C/C++ 编码规范 > **适用项目**: GCS 地面控制站 > **语言标准**: C++17 > **框架**: Qt 5/6 > **构建工具**: CMake 3.16+ > **格式化配置**: `.clang-format` (根目录) > **编码格式**: UTF-8 --- ## 1. 总则 ### 1.1 核心原则 | 原则 | 说明 | |------|------| | **一致性优先** | 新增/修改的代码必须与所在文件或模块的现有风格保持一致 | | **最小改动** | 仅修改目标代码,不随意改写无关代码、不随意添加无关注释 | | **编译安全** | 修改后必须能通过编译;增删成员时检查所有引用 | | **头文件自足** | 每个 `.h` 应包含其所有依赖的前向声明或 include | ### 1.2 编码原则 - 代码优先考虑**可读性**和**可维护性** - 避免过度抽象,优先使用简单方案 - 所有面向用户的字符串必须使用 `tr()` 包裹 - 禁止在新代码中使用裸 `new` / `delete` 管理资源(除非 Qt 父子关系托管的 QObject 构造) --- ## 2. 文件组织 ### 2.1 文件命名 | 类型 | 规范 | 示例 | |------|------|------| | 源文件 | `PascalCase.cpp` | `DataLink.cpp` | | 头文件 | `PascalCase.h` | `DataLink.h` | | UI 文件 | `PascalCase.ui` | `StatusUI.ui` | | 资源文件 | `snake_case.qrc` | `resources.qrc` | ### 2.2 目录结构 **当前项目实际结构**: ``` gcs_nf/ ├── App/ # 主程序入口 + MainWindow + AppController ├── PluginSDK/ # 插件开发包(静态库) ├── Plugins/ # 所有功能插件(每个插件一个目录) │ ├── IPlugin.h # 插件接口 v2 │ ├── IPlugin_v3.h # 插件接口 v3(向后兼容 v2) │ ├── Cockpit/ # 自绘飞行仪表盘 │ ├── MavLinkNode/ # MAVLink 协议解析 │ ├── dlink/ # 数据链路层 │ ├── opmap/ # 离线地图引擎 │ ├── Skin/ # 主题/皮肤 │ ├── Setting/ # 系统设置 │ └── ... ├── mavlink/ # MAVLink C 库(git submodule) ├── docs/ # 文档 ├── bin/ # 构建产物输出目录 └── .clang-format # 代码格式化配置 ``` **文件组织约定**: - 每个插件目录下至少有一个与插件同名的 `.h` / `.cpp` 主文件 - 使用 `inc/` / `src/` 分离头文件和源文件的模块(如 Cockpit、MavLinkNode),新增文件也遵循此布局 - UI 文件与使用它的类放在同一目录 - 插件目录下必须有 `plugin.json` 清单文件 ### 2.3 Include 顺序 遵循 **Google C++ Style** 的 include 顺序,各组之间空一行分隔: ``` 1. 自身对应的头文件(仅 .cpp 中): #include "MyClass.h" 2. C 系统头文件: #include 3. C++ 标准库头文件: #include 4. 第三方库头文件 (Qt): #include 5. 本项目头文件: #include "PluginSDK/PluginContext.h" ``` **示范**(`.cpp` 文件): ```cpp #include "PluginContext.h" #include #include #include #include #include "ServiceRegistry.h" ``` **示范**(`.h` 文件): ```cpp #ifndef PLUGINCONTEXT_H #define PLUGINCONTEXT_H #include #include "MavLinkService.h" ``` **禁止事项**: - **禁止** 用 `""` 包含系统头文件或 Qt 头文件(如 `#include "QObject"`),一律使用 `<>` - **禁止** 在头文件中包含不必要的头文件,优先使用前向声明 --- ## 3. 命名规范 ### 3.1 命名总表 | 元素 | 规范 | 示例 | |------|------|------| | 类名 / 结构体名 | `PascalCase` | `MavLinkNode`, `PluginContext` | | 函数 / 方法 | `camelCase` | `setPortName()`, `onDataReceived()` | | 成员变量 | `m_camelCase` | `m_portName`, `m_isConnected` | | 静态常量 / 枚举值 | `kPascalCase` 或 `UPPER_SNAKE` | `kDefaultBaud` 或 `DEFAULT_TIMEOUT` | | 局部变量 | `camelCase` | `portName`, `resultCount` | | 全局变量 | `g_camelCase` | `g_appConfig` | | 命名空间 | `snake_case` | `mapcontrol`, `plugin_utils` | | 宏 | `UPPER_SNAKE` | `MAVLINK_START_UART_PORT` | | 信号 (signal) | camelCase(过去式/完成态) | `dataReceived`, `connectionStateChanged` | | 槽 (slot) | `on` + 过去式 | `onDataReceived`, `onStartClicked` | | 枚举类型 | `PascalCase` | `ConnectionState` | | 头文件守卫宏 | `FILENAME_H` | `MAVLINKSERVICE_H` | ### 3.2 命名细则 **类名**: ```cpp class MavLinkNode : public QObject { }; // 正确 - PascalCase class mavlink_node : public QObject { }; // 错误 - snake_case ``` **成员变量**: ```cpp private: QString m_portName; // 正确 - m_camelCase bool m_isConnected = false; // 正确 QString portName; // 错误 - 缺少 m_ 前缀 ``` **信号 / 槽**: ```cpp signals: void dataReceived(const QByteArray &data); // 正确 - 过去式 void connectionStateChanged(bool connected); // 正确 - 过去式 void signal_heartbeat(); // 弃用 - 旧 MavLinkNode 风格,新代码禁止 public slots: void onDataReceived(const QByteArray &data); // 正确 void onStartClicked(); // 正确 void slot_data_received(); // 错误 - 弃用前缀 ``` **枚举**: ```cpp enum class ConnectionState { // 正确 - enum class Disconnected, Connecting, Connected, Error }; enum ErrorCode { // 旧风格(已有代码兼容) ERR_NONE = 0, ERR_TIMEOUT, ERR_PARSE }; ``` --- ## 4. 格式规范 ### 4.1 格式化工具 项目根目录有 `.clang-format`,**每次编辑后必须运行**: ```bash clang-format -style=file -i <修改的文件> ``` ### 4.2 核心格式规则 从 `.clang-format` 提取: | 规则 | 值 | |------|-----| | 基础风格 | LLVM | | 缩进宽度 | 4 空格 | | Tab | 禁用(一律空格) | | 行宽上限 | 120 字符 | | 大括号风格 | **Allman**(大括号独立一行) | | 指针/引用对齐 | **靠左** (`int *p`, `QString &s`) | | 访问修饰符缩进 | -4(`public:` / `private:` 向外凸出) | | include 排序 | **不自动排序**(手动管理) | | 最大连续空行 | 1 行 | | 短函数单行 | 允许(如空构造/析构) | ### 4.3 Allman 大括号示范 ```cpp void MyClass::doWork() { if (condition) { // ... } else { // ... } } class MyClass : public QObject { Q_OBJECT public: explicit MyClass(QObject *parent = nullptr); ~MyClass() override = default; signals: void finished(); private: int m_count = 0; }; ``` ### 4.4 空行与空格 - 类内各组之间空一行:`public:` / `signals:` / `public slots:` / `private:` 之间 - 函数体之间空一行 - 逻辑相关代码块可以内部不空行,不同逻辑块之间空一行 - `if` / `for` / `while` 关键字后跟一个空格再跟 `(` --- ## 5. 注释规范 ### 5.1 注释语言 - 代码注释使用**中文** - 面向用户的字符串使用 `tr()` 包裹中文 ### 5.2 注释格式 **类/结构体注释** — 使用 `/* */` 或 `//` 块注释(重要类使用多行): ```cpp // ============================================================ // AppController — 集中管理所有模块间的信号路由 // ============================================================ class AppController : public QObject { Q_OBJECT // ... }; ``` **函数注释** — 对公开接口和复杂逻辑的函数使用 Doxygen 风格: ```cpp /** * @brief 将伺服器 PWM 值转换为角度 * @param pwm 输入的 PWM 值 * @param offset 修正偏移 * @return 转换后的角度值(度) */ double pwmToDeg(int pwm, double offset); ``` **内联注释** — 对关键算法或非直观逻辑使用行尾注释: ```cpp painter.translate(width() * H_pos / 100, height() * V_pos / 100); /* 坐标变换为窗体中心 */ int side = qMin(width(), height()); /* 决定了模块只能是方形 */ ``` ### 5.3 注释禁止事项 - **禁止** 保留被注释掉的"死代码"(`// old code ...`)。提交前必须删除 - **禁止** 中文内容出现乱码(确保文件保存为 UTF-8 编码) - **禁止** 注释写"废话"(如 `// i 自增 1` 对 `i++` 的注释) --- ## 6. 头文件规范 ### 6.1 头文件守卫 **统一使用 `#ifndef` / `#define` / `#endif`**,宏名使用 `文件名大写_H` 格式,结尾带宏名注释: ```cpp #ifndef PLUGINCONTEXT_H #define PLUGINCONTEXT_H // ... 头文件内容 ... #endif // PLUGINCONTEXT_H ``` **禁止使用 `#pragma once`**(与项目现有代码保持一致)。 ### 6.2 前向声明 在 `.h` 中,如果只需要类型的指针或引用,优先使用前向声明而非 include: ```cpp // MyPlugin.h class QAction; class QWidget; namespace Ui { class MyPlugin; } namespace mapcontrol { class OPMapWidget; } class Config; ``` ### 6.3 头文件内容顺序 ```cpp #ifndef MYCLASS_H #define MYCLASS_H // 1. 必要的 Qt 头文件 #include // 2. 前向声明 class Config; namespace Ui { class MyClass; } // 3. 类定义 class MyClass : public QObject { Q_OBJECT public: // 构造/析构 // 公开方法 signals: // 信号 public slots: // 公开槽 protected: // 保护成员 private: // 私有成员 }; #endif // MYCLASS_H ``` --- ## 7. 类设计规范 ### 7.1 类成员可见性 **从上到下**的顺序: ``` public: 构造/析构 → 公开方法 signals: Qt 信号 public slots: Qt 公开槽 protected: 保护成员(含虚函数) private: 私有成员变量和方法 ``` ### 7.2 构造与析构 **使用 `explicit` 修饰单参数构造函数**: ```cpp class PluginContext : public QObject { Q_OBJECT public: explicit PluginContext(const QString &pluginId, QObject *parent = nullptr); ~PluginContext() override = default; }; ``` - QObject 子类使用 `QObject *parent` 作为最后一个参数,利用 Qt 父子内存管理 - 成员变量**就地初始化**(C++11 语法): ```cpp private: QString m_pluginId; // 不需要就地初始化(QString 默认已空) ServiceRegistry *m_serviceRegistry = nullptr; MavLinkService *m_mavlinkService = nullptr; bool m_isConnected = false; int m_retryCount = 3; ``` - 析构函数中手动 delete 非 QObject 子类的裸指针,并设为 nullptr - 使用 `Q_UNUSED()` 标记未使用的参数: ```cpp void MainWindow::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) // ... } ``` ### 7.3 虚函数重写 **重写虚函数必须加 `override` 关键字**: ```cpp class MyPlugin : public IPlugin { public: QString name() const override; bool initialize(PluginContext *context) override; void shutdown() override; protected: void paintEvent(QPaintEvent *event) override; // QWidget 虚函数 }; ``` ### 7.4 UI 文件使用 当类使用 `.ui` 文件时: ```cpp // StatusUI.h namespace Ui { class StatusUI; } class StatusUI : public QWidget { Q_OBJECT public: explicit StatusUI(QWidget *parent = nullptr); ~StatusUI() override; private: Ui::StatusUI *ui = nullptr; // 使用 m_ 前缀例外:此处用 ui }; // StatusUI.cpp #include "StatusUI.h" #include "ui_StatusUI.h" StatusUI::StatusUI(QWidget *parent) : QWidget(parent) , ui(new Ui::StatusUI) { ui->setupUi(this); QFile file(":/qss/StatusUI.qss"); file.open(QFile::ReadOnly); QTextStream filetext(&file); QString stylesheet = filetext.readAll(); setStyleSheet(stylesheet); file.close(); } StatusUI::~StatusUI() { delete ui; } ``` --- ## 8. Qt 特定规范 ### 8.1 Signal / Slot 连接方式 **新代码优先使用函数指针语法**: ```cpp // 推荐 — 编译期检查 connect(m_button, &QPushButton::clicked, this, &MyClass::onButtonClicked); // 带 lambda connect(m_timer, &QTimer::timeout, this, [this]() { updateStatus(); }); // 信号到信号 connect(m_source, &Source::dataReady, m_target, &Target::onDataReady); ``` **旧式 `SIGNAL()` / `SLOT()` 宏语法仅在维护旧代码时沿用,新代码禁止**: ```cpp // 弃用 — 仅维护旧代码时保留 connect(sender, SIGNAL(valueChanged(int)), receiver, SLOT(onValueChanged(int))); ``` ### 8.2 QObject 内存管理 - QObject 子类通过 `parent` 参数加入 Qt 对象树,由 Qt 自动负责析构 - 非 QObject 的成员指针需要在析构函数中手动 delete ```cpp // QObject 子类 — 不需要手动 delete m_button = new QPushButton(tr("开始"), this); // this 为 parent // 非 QObject 裸指针 — 必须在析构函数中 delete MyClass::~MyClass() { delete m_rawData; m_rawData = nullptr; } ``` ### 8.3 QString 使用 - **所有**界面字符串用 `tr()` 包裹: ```cpp m_label->setText(tr("连接已建立")); m_button->setToolTip(tr("点击以断开连接")); ``` - 数值格式化使用 `QString::number()`: ```cpp QString value = QString::number(pwm, 'f', 2); // 保留 2 位小数 QString hex = QString::number(address, 16).toUpper(); // 十六进制大写 ``` - 多段拼接使用 `arg()` 或 `QStringLiteral`: ```cpp QString msg = tr("端口: %1, 波特率: %2").arg(portName).arg(baudRate); ``` - **禁止** 在界面代码中使用 `std::string`,除非是纯算法库内部 ### 8.4 自绘组件(无 .ui 文件) 对于 Cockpit 等纯自绘组件,遵循以下模式: ```cpp // Cockpit.h class Cockpit : public QWidget { Q_OBJECT protected: void paintEvent(QPaintEvent *event) override; void resizeEvent(QResizeEvent *event) override; private: QColor m_groundColor = QColor(100, 100, 100); qreal m_roll = 0.0; }; // Cockpit.cpp void Cockpit::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); // ... 绘制逻辑 } ``` ### 8.5 qmake `.pro` / `.pri` 兼容 当前项目同时存在 CMake 和 qmake 构建系统。如果修改涉及 `.pro` 文件: - 保持 `.pri` 文件与 `CMakeLists.txt` 中的源文件列表同步 - 新增文件需同时添加到两个构建文件中 --- ## 9. 错误处理与日志 ### 9.1 日志等级与使用 ```cpp qDebug() << "调试信息"; // 开发期间输出,发布前删除或注释 qInfo() << "关键操作日志"; // 应保留,用于运行时追踪 qWarning() << "非致命异常"; // 如文件打开失败、配置缺失等 qCritical() << "严重错误"; // 可能导致功能不可用的错误 ``` ### 9.2 错误处理模式 **不抛出异常**(项目不使用 try/catch/throw): ```cpp // 检查条件,提前返回 QFile file(path); if (!file.open(QIODevice::ReadOnly)) { qWarning() << "无法打开文件:" << path << file.errorString(); return false; } // 检查空指针 if (!config) { qWarning() << "Config 未初始化"; return; } ``` ```cpp // JSON 解析错误处理 QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { qWarning() << "JSON 解析失败:" << error.errorString(); return; } ``` ### 9.3 断言 - 对不应发生的逻辑错误使用 `Q_ASSERT`(Debug 模式生效,Release 模式移除) - 对 Release 也需要检查的条件使用普通 if 判断 ```cpp Q_ASSERT(plugin != nullptr); Q_ASSERT(!m_pluginId.isEmpty()); ``` --- ## 10. 插件开发规范 ### 10.1 插件接口 当前项目同时支持两版插件接口: - **IPlugin v2** ([IPlugin.h](file:///d:/20_AI/gcs_nf/Plugins/IPlugin.h)) — 所有现有插件使用 - **IPlugin v3** ([IPlugin_v3.h](file:///d:/20_AI/gcs_nf/Plugins/IPlugin_v3.h)) — 基于订阅模式,新功能开发优先使用 ### 10.2 新插件模板 ```cpp // MyPlugin.h #ifndef MYPLUGIN_H #define MYPLUGIN_H #include "IPlugin_v3.h" class MyPlugin : public IPlugin_v3 { Q_OBJECT public: explicit MyPlugin(QObject *parent = nullptr); ~MyPlugin() override = default; // IPlugin 接口 QString name() const override; QString version() const override; bool initialize(PluginContext *context) override; void shutdown() override; private: PluginContext *m_context = nullptr; }; #endif // MYPLUGIN_H ``` ### 10.3 plugin.json 清单 每个插件目录必须有 `plugin.json`: ```json { "name": "MyPlugin", "version": "1.0.0", "description": "插件描述", "activationEvents": ["onStartup"], "contributes": {} } ``` --- ## 11. CMake 构建规范 ### 11.1 CMake 文件位置 - 顶层 `CMakeLists.txt` — 全局设置 + `add_subdirectory` - 每个模块目录下的 `CMakeLists.txt` — 模块自身的构建规则 ### 11.2 新增文件时的操作 1. 将 `.h` / `.cpp` 文件加入对应模块的 `CMakeLists.txt` 中的 `add_library` 或 `add_executable` 源文件列表 2. 如模块有 `.pri` 文件,同步更新 3. 新增 `Q_OBJECT` 类时,无需额外操作(`AUTOMOC` 已开启) --- ## 12. AI 编辑指南 > 本章专门面向 AI 辅助编码场景,指导 AI 如何在此项目中安全地编辑代码。 ### 12.1 编辑前的必读步骤 1. **读取目标文件** — 使用 Read 工具读取待编辑文件的完整内容 2. **理解上下文** — 检查该文件用到的所有类型、成员、信号槽 3. **确认风格** — 观察现有代码的命名、缩进、注释风格,严格模仿 4. **检查依赖** — 查看 `#include` 列表,确保新增代码的依赖已包含 ### 12.2 编辑操作规范 | 操作 | 规范 | |------|------| | 新增类 | 创建一个 `.h` + `.cpp`,遵循类设计规范(第 7 章) | | 新增成员函数 | 在 `.h` 中声明,在 `.cpp` 中实现;检查是否需要 `override` | | 新增成员变量 | 使用 `m_camelCase` 命名,放在 `private:` 区,作为最后手段才放 `public:` | | 新增信号 | 放在 `signals:` 区,使用过去式命名 | | 新增槽 | 放在 `public slots:` 或 `private slots:`,使用 `onXxx` 命名 | | 修改函数体 | 仅修改目标函数,不随意重构无关代码 | | 添加 include | 插入到 include 分组中的正确位置 | | 删除代码 | 彻底删除包括相关注释和空行,不留"死代码" | ### 12.3 编辑后的必做检查 - [ ] 新增/修改的类是否添加了 `Q_OBJECT` 宏 - [ ] 是否使用了 `tr()` 包裹所有用户可见字符串 - [ ] 重写的虚函数是否加了 `override` - [ ] 单参数构造函数是否加了 `explicit` - [ ] 是否修复了 `#include` 顺序 - [ ] 是否有匹配的 `#ifndef` / `#define` / `#endif` 守卫(新文件) - [ ] 修改后是否运行了 `clang-format -style=file -i <文件>` - [ ] 是否同步更新了 `CMakeLists.txt` ### 12.4 常见编辑场景示范 #### 场景 A:给现有类添加一个槽函数 ``` 步骤: 1. 读取 MyClass.h,在 public slots: 或 private slots: 区添加声明 2. 读取 MyClass.cpp,在文件末尾添加函数实现 3. 使用 clang-format 格式化 ``` #### 场景 B:创建一个新的简单对话框 ``` 步骤: 1. 在某插件目录下创建 MyDialog.h / MyDialog.cpp / MyDialog.ui 2. MyDialog.h:按照 7.4 节 UI 文件使用规范编写 3. MyDialog.cpp:实现构造/析构函数 4. 同步更新 CMakeLists.txt 5. 使用 clang-format 格式化所有新文件 ``` #### 场景 C:在现有文件中添加一个信号 ``` 步骤: 1. 读取头文件,在 signals: 区添加信号声明 2. 确认信号使用过去式名称(如 dataReady, finished) 3. 查找所有用到信号的地方(通过 grep),确认连接方式 ``` ### 12.5 代码样式速查卡片 ``` 命名: 类 PascalCase | 成员 m_camelCase | 信号过去式 | 槽 onXxx 格式: Allman 大括号 | 4 空格缩进 | 120 列宽 | 指针靠左 int *p 头文件: #ifndef_H 守卫 | #include <> 系统头 | #include "" 项目头 类结构: public → signals → public slots → protected → private 字符串: tr("中文字符串") | QString::number(x, 'f', 2) 调试: qDebug / qInfo / qWarning | 不用 try/catch 构造: explicit 单参构造 | QObject *parent = nullptr 最后 | 成员就地初始化 重写: 所有虚函数重写加 override 插件: 实现 IPlugin_v3 | 提供 plugin.json | 在 CMakeLists.txt 注册 编辑后: 运行 clang-format | 检查 tr() | 检查 CMakeLists.txt 同步 ``` --- ## 附录 A:与旧代码风格兼容说明 项目中有大量 2020-2022 年编写的旧代码,其风格与本文规范有所不同。AI 在修改旧文件时: | 旧风格 | 新风格(本文规范) | 处理方式 | |--------|-------------------|---------| | `signal_heartbeat()` | `heartbeatReceived()` | **仅新增信号**用新风格;已有旧信号保持不变 | | `SIGNAL(...)` / `SLOT(...)` | `&Class::method` | **新增 connect** 用新语法;已有旧连接保持不变 | | 无 `override` | 添加 `override` | 修改到旧函数时可顺手补上 | | 无 `explicit` | 添加 `explicit` | 修改到旧类时可顺手补上 | | `#include "QObject"` | `#include ` | 修改到 include 时可顺手修正 | | public 成员变量 | private + m_ 前缀 | **仅新增成员**用新风格;已有 public 成员保持不变 | | 中文注释乱码 | UTF-8 中文注释 | 修改到乱码注释附近时修正 |