这是一篇规范指南,前些天看《C++开发规范》有感,说了很多关于C++/Qt的代码规范,读完提升了很多。要是所有人都能完全按照这个指南进行开发,那就再也不会有屎山代码了!
1. 命名约定
- 清晰性: 名称应清晰地表达其目的或内容。避免缩写,除非是广泛接受且不会引起歧义的(如
num,str,buf)。 - 一致性: 在整个项目中保持命名风格一致。
1.1 类、结构体、枚举、命名空间
-
规则: 使用
PascalCase(大驼峰命名法)。 -
示例:
cppclass MyWidget; struct UserSettings; enum class LogLevel { Debug, Info, Warning, Error }; namespace NetworkUtils;
1.2 函数和方法
-
规则: 使用
camelCase(小驼峰命名法)。 -
示例:
cppvoid calculateTotalPrice(); QString getDisplayName() const; bool isValidInput();
1.3 变量(局部变量、成员变量、全局变量)
-
规则: 使用
snake_case(蛇形命名法)。 -
成员变量: 建议为成员变量添加前缀
m_以区别于局部变量。 -
常量: 使用
k前缀或全大写SNAKE_CASE(视项目约定而定)。 -
示例:
cppint item_count; QString m_user_name; // 成员变量 double total_amount; static constexpr int kMaxRetries = 5; // 常量 const double PI = 3.1415926535; // 或 PI 全大写
1.4 宏
-
规则: 使用全大写
SNAKE_CASE。 -
建议: 在 Qt 中应尽量避免使用宏,优先使用
constexpr、内联函数或模板。如果必须使用,确保名称独特且不易冲突。 -
示例:
cpp#ifdef Q_OS_WIN #define PLATFORM_SPECIFIC_SETTING 1 #endif
2. 格式与布局
-
缩进: 使用 4 个空格 进行缩进。不要使用制表符 (Tab)。配置编辑器以确保按 Tab 键时插入空格。
-
大括号
{}:-
K&R / Stroustrup 风格: 左大括号
{放在语句的同一行末尾,右大括号}单独一行并与语句开始处对齐。这是 Qt 项目中常见的风格。 -
示例:
cppif (condition) { // ... } else { // ... } for (int i = 0; i < count; ++i) { // ... } void myFunction() { // ... }
-
-
行长度: 建议每行不超过 80 或 120 个字符(团队内部统一)。如果超出,应在逻辑清晰的位置(如逗号后、运算符前)进行换行,新行缩进一级。
-
空格:
-
在二元运算符(如
=,+,-,*,/,%,<<,>>,<,>,<=,>=,==,!=,&&,||,&,|,^)前后添加空格。 -
在逗号
,和分号;后添加空格。 -
在控制流语句(
if,for,while,switch)和左括号(之间添加空格。 -
函数调用和函数声明中,函数名与左括号
(之间不加空格。 -
不要在
.,->,::运算符前后加空格。 -
不要在单目运算符(如
!,~,++,--,&(取址),*(解引用))和其操作数之间加空格。 -
不要在
[],()内部紧贴括号的地方加多余空格。 -
示例:
cppint result = a + b * (c - d); // 运算符空格 if (flag) { ... } // if 后空格 myFunction(arg1, arg2); // 函数名后无空格,逗号后有空格 object->method(); // -> 前后无空格 int *ptr = &var; // * & 是单目运算符,无空格 array[5] = value; // [] 内无多余空格
-
-
空行:
- 使用空行将逻辑相关的代码块分开,提高可读性。
- 在函数定义之间、类定义内部不同部分(如 public, private, signals, slots)之间、长的函数内部逻辑段落之间添加空行。
- 避免连续多个空行。
-
文件组织:
-
头文件 (.h, .hpp):
cpp#pragma once // 或 #ifndef ... #define ... #endif // 必要的头文件包含 (先系统头文件,后第三方库头文件,再项目头文件) #include <vector> #include <QWidget> #include "utils.h" // 前向声明 (如果需要) class MyOtherClass; // 类/命名空间声明 namespace MyNamespace { class MyClass : public QWidget { Q_OBJECT // Qt 元对象系统宏 public: explicit MyClass(QWidget *parent = nullptr); ~MyClass() override; // ... 其他公共成员 signals: // ... 信号 public slots: // ... 公共槽 private slots: // ... 私有槽 private: // ... 私有成员变量和方法 }; } // namespace MyNamespace -
源文件 (.cpp):
cpp// 包含对应的头文件 #include "myclass.h" // 其他必要的头文件 #include <QDebug> // 实现 namespace MyNamespace { MyClass::MyClass(QWidget *parent) : QWidget(parent) { // 构造函数实现 } MyClass::~MyClass() { // 析构函数实现 (如果需要清理资源) } // ... 其他成员函数的实现 } // namespace MyNamespace
-
3. 内存管理
-
所有权: 明确对象的所有权关系(谁创建,谁负责销毁)。
-
Qt 父对象机制: 充分利用 Qt 的父子对象机制。当创建一个
QObject派生类对象(如QWidget)时,如果指定了父对象,父对象销毁时会自动销毁其所有子对象。这是管理 GUI 对象生命周期的首选方式。cppQPushButton *button = new QPushButton("Click Me", this); // 'this' 是父窗口 // 当父窗口销毁时,button 也会被自动销毁 -
智能指针:
- 对于非
QObject派生类对象,或者所有权需要在不同作用域间转移的对象,优先使用智能指针 (std::unique_ptr,std::shared_ptr)来管理内存,避免手动new/delete。 QPointer: 当持有指向QObject派生类对象的指针时,如果该对象可能被 Qt 的父子机制在其他地方销毁,应使用QPointer<T>。它是一个弱指针,当目标对象被销毁时,它会自动设置为nullptr,防止悬空指针。
cppQPointer<QPushButton> weakButton = button; // button 是 QPushButton* // ... 其他地方可能删除了 button 指向的对象 if (weakButton) { // 安全检查,避免访问已销毁对象 weakButton->setText("Safe"); } - 对于非
-
避免手动
delete: 除非有明确的理由(如性能关键路径,且对象生命周期非常清晰),否则尽量避免手动调用delete。优先依赖父对象机制或智能指针。 -
资源管理: 使用 RAII (Resource Acquisition Is Initialization) 原则管理文件句柄、网络连接、锁等资源。利用构造函数获取资源,析构函数释放资源。
4. Qt 特有特性规范
-
QObject和Q_OBJECT宏:- 所有派生自
QObject的类都必须 在类定义的private区域包含Q_OBJECT宏(通常紧跟在类名和基类列表之后)。 - 这启用了信号槽机制、运行时类型信息 (
qobject_cast)、属性系统等。
- 所有派生自
-
信号 (
signals):-
在
signals:区域声明信号。 -
信号只需声明,不要实现。
-
信号名通常使用动词的一般现在时或描述状态变化的短语。
-
信号参数应尽量少且简单。避免传递复杂或需要手动管理内存的类型(如原始指针)。优先使用值类型或 Qt 的隐式共享类(如
QString,QImage)。 -
示例:
cppsignals: void buttonClicked(); void progressChanged(int percent); void dataReady(const QByteArray &data); // 使用 const 引用
-
-
槽 (
slots):-
在
public slots:,protected slots:,private slots:区域声明槽函数。 -
槽函数是普通的成员函数,需要实现。
-
槽函数命名应清晰反映其功能。可以使用
on_ObjectName_signalName的命名约定来自动连接(如果使用 Qt Designer 的 UI 文件),但这不是强制的。 -
示例:
cpppublic slots: void handleButtonClick(); // 通用命名 void on_pushButton_clicked(); // UI 自动连接命名
-
-
信号槽连接 (
connect):-
优先使用 Qt5 的新式连接语法 (基于函数指针): 它提供编译时类型检查,更安全。
-
示例:
cppconnect(sender, &SenderClass::signalName, receiver, &ReceiverClass::slotName); connect(button, &QPushButton::clicked, this, &MyWidget::handleButtonClick); -
避免使用基于字符串的旧式
SIGNAL()和SLOT()宏,除非连接QObject动态属性或需要运行时灵活性(这种情况较少)。
-
-
属性系统 (
Q_PROPERTY):-
使用
Q_PROPERTY宏声明类的属性,使其可被元对象系统访问(如用于 QML)。 -
定义相应的读取函数(
READ getter)、写入函数(WRITE setter)和通知信号(NOTIFY signal)。 -
示例:
cppQ_PROPERTY(QString userName READ userName WRITE setUserName NOTIFY userNameChanged)
-
-
事件处理 (
event,eventFilter):- 重写
QObject::event(QEvent *)或特定的事件处理器(如mousePressEvent,keyPressEvent)来处理事件。 - 使用事件过滤器 (
installEventFilter,eventFilter) 时,注意过滤器的安装和移除时机。 - 在事件处理器中,如果需要处理特定事件后还要让事件继续传播,调用基类的实现(如
QWidget::mousePressEvent(event))。
- 重写
-
线程 (
QThread):QObject的线程亲和性: 每个QObject实例都与一个线程(通常是创建它的线程)相关联。其子对象也属于同一线程。不要在非创建线程中访问该对象的成员(信号槽跨线程调用是安全的)。QThread使用模式:- Worker-Object 模式 (推荐): 创建一个工作对象(派生自
QObject),将其moveToThread到一个QThread实例中。在该对象中使用信号槽进行线程间通信。QThread的事件循环负责执行该对象槽函数的调用。 - 避免直接继承
QThread并重写run()方法,除非有非常特殊的低级需求。
- Worker-Object 模式 (推荐): 创建一个工作对象(派生自
- 线程安全: 使用互斥锁 (
QMutex,QMutexLocker)、读写锁 (QReadWriteLock)、信号量 (QSemaphore) 或原子操作来保护多线程共享数据。尽量减少共享数据。
5. 其他 C++ 最佳实践
-
const正确性:- 将不会修改成员变量的成员函数声明为
const。 - 使用
const引用传递不希望被修改的大型对象或参数。 - 尽可能使用
const变量。
cppint calculate() const; // const 成员函数 void processData(const QString &data); // const 引用参数 const double kFactor = 1.234; // const 常量 - 将不会修改成员变量的成员函数声明为
-
初始化:
- 使用初始化列表初始化成员变量,尤其是在基类有非默认构造函数或成员变量是引用/常量时。
- 避免在构造函数体内进行赋值初始化。
cppMyClass::MyClass(int value, const QString &name) : BaseClass(value), // 基类初始化 m_name(name), // 成员变量初始化 m_counter(0) { // 构造函数体 } -
类型转换:
- 避免 C 风格转换: 如
(int) someFloat。 - 优先使用 C++ 风格转换:
static_cast: 用于明确定义的、相对安全的转换(如数值类型转换、基类指针到派生类指针(当你知道安全时))。dynamic_cast: 用于在继承层次中进行安全的向下转型(需要运行时类型信息 RTTI)。适用于QObject派生类(使用qobject_cast更高效)。const_cast: 移除const或volatile属性(谨慎使用)。reinterpret_cast: 低级的、与实现相关的重新解释(高度危险,尽量避免)。
qobject_cast: 用于在QObject继承树中进行安全的向下转型。比dynamic_cast更快,因为它利用 Qt 的元对象系统。
cppQWidget *widget = ...; QPushButton *button = qobject_cast<QPushButton*>(widget); if (button) { // 转换成功 } - 避免 C 风格转换: 如
-
错误处理:
- 异常: 在 Qt 中,异常的使用存在一些争议。Qt 本身在错误情况下(如内存分配失败)通常不抛出异常。团队需要统一策略:是使用异常还是错误码/返回值。
- 断言: 使用
Q_ASSERT,Q_ASSERT_X在调试版本中检查程序内部逻辑不变量的成立。它们只在Debug构建中有效,在Release构建中会被移除。
cppvoid MyClass::setValue(int v) { Q_ASSERT_X(v >= 0 && v <= 100, "MyClass::setValue", "Value out of range [0,100]"); m_value = v; } -
nullptr: 使用nullptr表示空指针,而不是NULL或0。 -
范围
for循环: 优先使用基于范围的for循环遍历容器。cppQList<QString> list; for (const QString &str : list) { // ... } -
auto: 谨慎使用auto。在类型明显且冗长(如迭代器类型)或模板代码中可以提高可读性。避免在类型不明显或对理解代码至关重要的地方使用auto。cppauto button = new QPushButton(this); // 类型明显 (QPushButton*) for (auto it = list.begin(); it != list.end(); ++it) { // 迭代器类型可能冗长 }
6. 文档与注释
-
API 文档: 使用 Doxygen 风格的注释为公共 API(类、方法、属性、枚举等)添加文档。
cpp/** * @brief 计算两个数的和。 * @param a 第一个加数。 * @param b 第二个加数。 * @return 两个加数的和。 */ int add(int a, int b); -
代码注释: 在代码内部,对复杂的逻辑、算法、非显而易见的决策、重要的注意事项或
TODO/FIXME标记添加注释。避免注释那些从代码本身就能清晰理解的内容。 -
TODO/FIXME: 使用一致的标记来标注需要后续完善或修复的地方。cpp// TODO: Optimize this algorithm for large datasets. // FIXME: This workaround might break on Windows.
7. 工具
- 代码格式化工具: 使用
clang-format等工具自动格式化代码,确保团队风格一致。可以配置.clang-format文件定义规则。 - 静态分析工具: 使用
Clang-Tidy、Cppcheck或 Qt Creator 内置的分析工具来检查潜在问题(如未定义行为、内存泄漏隐患、编码规范违反等)。 - 版本控制: 使用 Git 等版本控制系统管理代码。遵循良好的分支策略(如 Git Flow)和提交信息规范(清晰描述修改内容)。