Qt/QML 编码规范
一. 概述
良好的编码规范可以大幅提高程序的可读和可理解性,最终目标是实现更友好的可维护性。
保持良好的开发和编码规范也是一种利己利人的高效习惯,值得每一位开发者去锻炼、养成和坚持。
本规范仅是建议,不能强求一律,允许权衡例外。
All for one, one for all —— 人人为我,我为人人。合作之道,就在其中。
二. C++ 相关的约定
- 不要使用异常
- 不要使用 rtti (运行时类型信息;即typeinfo结构、dynamic_cast或typeid操作符,包括抛出异常)
- 明智地使用模板,而不是因为你可以这样做。(Use templates wisely, not just because you can.)
三. Qt/QML 统一规范
- 行尾不能有空格
- 每个 QObject 子类都必须加上 Q_OBJECT 宏,即使它没有定义任何信号或插槽,否则使用 qobject_cast 将失败,此时倒不如不继承 QObject
- 你可以将本文档真的 <SomeOne> 替换为你的公司名称
四. Qt 编码规范
1. 头文件
(1) 头文件保护
所有头文件都应该使用 #define 防止头文件被多重包含,命名格式为:
// classname.h
#pragma once
#ifndef CLASSNAME_H_A37F2BB8_82AE_4426_87BD_8969C0000936
#define CLASSNAME_H_A37F2BB8_82AE_4426_87BD_8969C0000936
// ……
#endif //CLASSNAME_H_A37F2BB8_82AE_4426_87BD_8969C0000936
其中的 H_ 后的部分为 UUID ,这部分可以使用工具生成:Linux 和 Mac 系统可以使用命令行工具,Windows 系统可以使用uuid在线生成工具。
关于头文件保护的涉及到的宏的简介,可以参考《C++ 头文件保护》
(2) 头文件依赖
减少头文件中 #include 文件的数量,尽量使用前向声明:
# include<MustIncludeClassA>
//...
class SomeClassIncludeInCppA;
class SomeClassIncludeInCppB;
// ...
class MyClass {
// ...
};
(3) 头文件包含次序
将包含次序标准化可增强可读性,次序建议如下:
Qt 库的头文件、C/C++ 标准库头文件、其他三方库的头文件、项目内定义的头文件。
如果不得不包含私有头文件。使用以下语法,不管 whatever_p.h 在哪个模块或目录中。
#include <private/whatever_p.h>
2. 命名约定
(1) 通用命名约定
避免使用缩写
(2) 文件命名
文件名建议全部小写,可以包含下划线,例如:
some_class_test_a.h
some_class_test_a.cpp
someclasstestb.h
someclasstestb.cpp
(3) 类命名
类名是名词,每个单词以大写字母开头,不包含下划线,且以大写字母C开头,例如:
class CSomeClassA {
};
class CMyClassB {
};
(4) 变量命名
- 变量名是名词
- 首个单词以小写字母开头,后续单词以大写字母开头
- 每个变量占一行
- 单一字符的变量只在临时变量或循环计数中使用
- 类成员变量需在变量名前加 m_ 前缀,例如:
int m_myValue;
- 使用内部封装类作为 d 指针成员变量时,封装类内可不使用 m_ 前缀
- 局部变量要等到需要使用时再进行定义变量,且定义时必须进行初始化:整数用 0、实数用 0.0、指针用 nullptr、字符用 '\0'
- 尽量不要使用全局变量,以降低耦合
- bool 类型的变量尽量使用 enabled、visible、movable 这种形式
(5) 常量命名
常量不含前缀且应该大写,单词间有下划线,包括全局常量和宏定义,例如:
const int MY_DEFINE_NUMBER = 0;
//...
#define MY_DEFINE_VALUE 0
(6) 函数命名
函数命名力求通过名称就能达到使调用者知道其返回的是什么属性值,或者其调用之后的作用的目标。
因此,取值函数尽量使用名词或者带修饰词的名词,而赋值或者带来改变的函数采用动宾语法,例如 setWidgetMovable(),返回 bool 的函数可以使用 is 加属性名的方式,首个单词以小写字母开头,后续单词以大写字母开头,例如:
QString name() const;
QSize screenSize() const;
void setPoint(const QPoint& point);
void drawLine(const QPoint& startPoint, const QPoint& endPoint);
bool isMovable() const;
void setMovable(bool movable);
函数参数使用名词或者带限定修饰词的名词,尽量避免使用缩写,或者使用与 Qt 一致的缩写形式,首个单词以小写字母开头,后续单词以大写字母开头,参数类型如果不是基本数据类型,使用 const 引用作为参数,例如:
void drawRectangle(const QPoint& topLeft, const QPoint& bottomRight);
void drawRectangle(const QRect& rect);
函数及其参数的命名一定不要出现无意义或者根据其名称难以知道其作用的名称,例如:fun0、f1 、param0、p1 等名称。这种名称往往作者本人回头看代码时都要看下函数实现,才能知道当初自己想要做什么。
(7) 枚举命名
枚举名和枚举值都是名词,每个单词以大写字母开头,且枚举名的第一个单词是 <SomeOne>,例如:
enum SomeOneTitleBarBackgroundColor {
Transparent,
White,
Black,
Red
};
(8) 命名空间命名
命名空间的名称是名词,每个单词以大写字母开头,且前两个单词是 <SomeOne>,命名空间的结束大括号之后跟命名空间名称的注释,例如:
namespace SomeOneUtils {
//...
} // SomeOneUtils
(9) 结构体命名
- 结构体中只定义变量,不定义函数,需要定义函数请使用类
- 结构体名使用名词,每个单词以大写字母开头
- 结构体成员使用名词定义,首单词以小写字母开头,后续单词以大写字母开头
示例如下:
struct SuperDevice {
bool isSuperDevice;
QString name;
};
3. 注释约定
(1) 简单注释
- 单行注释:/// 或者 //!
- 多行注释:/** 或者 /*!
- 注释掉的代码使用://
- 行内注释掉代码:/* 和 */
(2) 类注释
类的头文件顶部需添加说明性注释,包括版权说明、版本号、作者、生成日期、类的功能描述等。
/**
* Copyright (C) 2020 Beijing <SomeOne> Technology Co.,Ltd. All rights reserved.
*
* @brief: doxygen 测试类
* 这个类是用于测试使用 doxygen 的实例类
* @author: ZhaoDongshuang
* @email: imtoby@126.com
* @version: 1.0.0
* @date: 2020-04-27 14:52
*
* This software, including documentation, is protected by copyright controlled
* by Beijing <SomeOne> Technology Co.,Ltd. All rights are reserved.
*/
(3) 常量、变量的注释
通常其命名时应做到足以说明用途,特定情况下,需要额外注释说明的话,一般可分两种形式,可以根据喜好选择,只要保证在整个工程中统一即可
- 代码上一行注释
//! 缓存大小
#define BUF_SIZE 1024*4
- 代码后注释
#define SIMPLE_PI 3.14 ///< 不精确的圆周率
(4) 函数注释
重要函数头部应该进行注释,包括函数名、函数功能描述、输入参数、输出参数、返回值及其他。以 main 函数为例:
/**
*
* @brief: 主函数
* @detail: 应用程序入口
* @param argc: 命令参数个数
* @param argv: 命令参数指针数组
* @return: 程序执行成功与否
* @retval 0: 程序执行成功
* @retval 1: 程序执行失败
* @note: 测试
* @attention: 注意
* @warning: 警告
* @exception: 异常
*/
int main(int argc, char* argv[])
{
// ...
return 0;
// ...
return 1;
}
(5) 代码块或代码行的注释
对于实现代码中重要或不容易理解的地方加以注释。建议采用代码块上方注释的形式:
//! 不使用中间变量实现交换变量的值,使用引用或指针都可以
void swap(int& a, int& b) {
a=a^b;
b=a^b;
a=a^b;
}
(6) TODO 注释
计划中但未完成的代码使用TODO注释,但是一定要对 TODO 的内容进行简单的说明,例如:
//! 不使用中间变量实现交换变量的值,使用引用或指针都可以
void swap(int& a, int& b) {
a=a^b;
b=a^b;
a=a^b;
}
// TODO: 添加使用中间变量实现交换变量的值的实现版本
(7) doxygen 其他的一些标注方式及说明
命令 | 生成字段名 | 备注 |
---|---|---|
@see | 参考 | |
@class | 引用类 | 用于文档生成连接 |
@var | 引用变量 | 用于文档生成连接 |
@enum | 引用枚举 | 用于文档生成连接 |
@code | 代码块开始 | 与@endcode成对使用 |
@endcode | 代码块结束 | 与@code成对使用 |
@bug | 缺陷 | 链接到所有缺陷汇总的缺陷列表 |
@todo | TODO | 链接到所有TODO 汇总的TODO 列表 |
@example | 使用例子说明 | |
@remarks | 备注说明 | |
@pre | 函数前置条件 | |
@deprecated | 函数过时说明 |
4. 代码排版约定
(1) 每一行的长度
行长度限定不超过 80 个字符,超出部分分成多行书写,长表达式要在较低优先级操作符处划分新行,操作符放在新行之首,逗号放在一行的结束,划分出的新行要进行适当的缩进,使排版整齐,语句可读,例如:
// Wrong
if (longExpression +
otherLongExpression +
otherOtherLongExpression) {
}
// Correct
if (longExpression
+ otherLongExpression
+ otherOtherLongExpression) {
}
(2) 缩进
使用 4 个空格进行代码缩进,不要用 Tab 键。但是对于由开发工具自动生成的代码可以有不一致。
预处理指令不要缩进,从顶格开始,例如:
if (isActive()) {
#if OS_WIN32
dropEverything();
#endif
backToNormal();
}
(3) 空白
-
空行可将语句进行适当的分组,便于阅读,在相对独立的代码块之间必须加一行空行,且始终只使用一行空行。
-
一定要在关键字之后和花括号之前使用单个空格:
// Wrong
if(foo){
}
// Correct
if (foo) {
}
- 对于指针或引用,总是在类型和'*'或'&'之间使用一个空格,但是在'*'或'&'和变量名之间不使用空格:
char *x;
const QString &myString;
const char * const y = "hello";
- 用空格包围二进制运算符
- 在每个逗号后面留一个空格
- 用于转换的右尖括号后不要有空格(避免C风格的转换)
// Wrong
char* blockOfMemory = (char* ) malloc(data.size());
// Correct
char *blockOfMemory = reinterpret_cast<char *>(malloc(data.size()));
- 在控制流语句中另起一行
// Wrong
if (foo) bar();
// Correct
if (foo) {
bar();
}
(4) 大括号
- 类、类方法的实现、全局方法的左大括号永远单独占一行,其他情况不单独占一行,跟在语句后面,例如:
static void foo(int g)
{
qDebug("foo: %i", g);
}
namespace SomeOneUtils {
bool isCEqualToAPlusB(int numA, int numB, int numC) {
if ((numA + numB) == numC) {
qDebug() << numA << "+" << numB "=" << numC;
return true;
}
return false;
}
class TestA
{
public:
void todoTest();
};
void TestA::todoTest()
{
if (isCEqualToAPlusB(1, 1, 2)) {
// ...
}
}
} // SomeOneUtils
- 条件语句即使只有一条主体内容,也建议使用大括号,以便可能的添加和阅读
- 即使条件语句的主体为空时,也使用大括号
// Wrong
while (a);
// Correct
while (a) {}
(5) 圆括号
使用圆括号将表达式分组,即使运算符的优先级相同,或者明确知道表达式的执行优先级,也要用圆括号进行分组,以便于阅读,例如:
// Wrong
if (a && b || c)
// Correct
if ((a && b) || c)
// Wrong
a + b & c
// Correct
(a + b) & c
(6) switch 语句
- case 和 switch 位于同一列
- 每个 case 必须在结尾有一个 break(或 return)语句,或者使用 Q_FALLTHROUGH() 来表明没有故意中断,除非另一个 case 紧随其后
switch (myEnum) {
case Value1:
doSomething();
break;
case Value2:
case Value3:
doSomethingElse();
Q_FALLTHROUGH();
default:
defaultHandling();
break;
}
5. 代码编写上的约定
- 代码采用 C++11 标准,尽量使用智能指针,尽量不使用裸指针(Qt 中使用 QScopedPointer)
- 使用 Qt5 新的信号和槽连接方式,信号和槽的命名不加 signal 和 slot 前缀,用动作和on动作方式,如信号 clicked(),槽为 onClicked()
- 关键代码需要有单元测试,非界面代码一般认为是关键代码
- 默认debug build前推荐使用静态代码检查工具(Cppcheck/Clang/VS)检查,编译时 使用 gcc sanitizer 增加地址和未定义行为动态检查
- 开启 qtcreator 插件 beautifier,并配置 clang-format 格式化工具,进行项目代码整体风格的统一管理,可以参考使用 Qt Creator 插件 beautifier 配置 clang-format 工具管理项目代码整体风格
- 在填入界面显示文字时,使用英文,通过 Qt Linguist 翻译成中文,以便多语言的实现,文件编码一律使用 UTF-8,添加 BOM
- 在绘制界面的时候,尽量使用 layout 和 stretch 来布局,尽量不要使用固定的大小,例如固定 10 像素
- 避免使用 C 类型的强制转换,选择 C++ 类型的强制转换(static_cast、const_cast、reinterpret_cast),虽说 reinterpret_cast 和 C 类型的强制转换都是危险的,但至少 reinterpret_cast 不会删除 const 修饰符
- 使用 qobject_cast 来代替 dynamic_cast
- 使用构造函数来转换简单类型: int(myFloat) 而不是 (int)myFloat
五. QML 编码规范
1. 文件命名
(1) QML 文件命名
- 仅由英文单词组成
- 以大写字母 C (Custom type 之意)开始,遵循大驼峰法的命名规则
示例:
CProgressDialog.qml
CListView.qml
CMainWindow.qml
(2) js 文件命名
- js 文件名由小写的英文单词和 _ 连接组成
- js 文件名一律以“ <someone> ”结尾(<someone>表示公司或组织名称)
2. 编码规范
(1) import 规则
qml 文件中,import 模块的建议顺序为:
- Qt 内置模块
- 框架自定义 Qt Quick 模块
- 项目自定义 Qt Quick 模块
- 本地 *.qml 组件包
- javascript 导入脚本文件
- javascript 引用脚本模块
示例:
// CPopupWinodw.qml
import QtQuick 2.12
import QtQuick.Controls 2.5
import com.facetoby.UITools.Image 1.0 as FaceImage
import com.facetoby.UITools.Video 1.0 as FaceVideo
import com.facetoby.GoodUtils 1.0 as FaceUtils
import common_tool_kits 1.0
import "self_tool_kits"
import "rename_tool_someone.js" as RenameTool
import "utils.js" as Utils
为了避免不同模块之间的命名冲突,我们在导入模块时可以使用“as”关键字设置该模块在本 qml 文件中的“别名”,对于"as"的相关规则:
- 导入 Qt 内置模块的版本号选择对应的 Qt 版本的最高版本
- 导入 js 文件到 qml 中必须要命名别名
- 导入模块,存在命名冲突时,则至少给其中一个模块设置别名
- 不对 Qt 内置模块设置别名
- 对于自定义模块需设置别名
- 别名需保持在当前 qml 文件内的唯一性
- 别名的命名规则采用大驼峰法
(2) qml 组件组成结构规则
qml 文件描述的实际上一个由 “UI组件节点” 组成的“树”。而每一个 qml 有有且只有一个“根节点”。我们可以称其为“根组件”。
跟组件的定义采用使 qml 文件复杂度最低原则,例如我们有如下 qml 文件:
// CPageBackground.qml
Item {
// ...
Rectangle {
// ...
}
}
则应改写为:
// CPageBackground.qml
Rectangle {
// ...
}
除非有不得不封装的理由,否则应以坚持降低 qml 的复杂度为首要原则。
(3) qml 组件封装的实现规则
qml 文件的根组件及新定义的属性、方法、信号,都对其使用者可见,因此按照约定俗成的添加双下划线("__")前缀的方式并不能避免用户有意的对相应的属性、方法、信号的调用。
因此,本规则建议将需要作为私有(private)的属性、方法、信号,包含在 QtObject 节点组件中。
示例:
// CPageBackground.qml
import QtQuick 2.2
Rectangle {
id: pageBackground
color: privateObject.defaultColor
// ... other codes
// private:
QtObject {
id: privateObject
objectName: "someonePageBackground"
property color defaultColor: "black"
// ...
function resetColor() {
pageBackground.color = defaultColor;
}
// ...
}
}
(4) qml 组件内容的定义规则
- 一行只写一条语句
- 使用 QML 进行显示,使用 C++/Qt 处理复杂的计算和数据管理逻辑
- Loader 出来的 qml 界面,在界面关闭时,一定要清空 Loader 资源
- js 代码中,能够用条件运算符(? :)代替 if...else... 地方,要用条件运算符,这样使代码更简洁明了
- 一个 qml 文件中,设置 id 的个数不要超过 7 个,多余 7 个,则最好进行 qml 代码的拆分,建议设置不超过 4 个 id 为宜,更小粒度的 qml 更加便于阅读和维护
- 当一个 qml 文件中,代码行达到 200 至 300 行时,要从其 qml 文件中分解出子 qml 文件,这样可以避免出现控件关联复杂,篇幅较长的代码。原因也是,更小粒度的 qml 更加便于阅读和维护
- 如果有归属同一类组的多个属性需要设置,为了提高代码可读性,则使用组的提示符,而不是点的运算符号
例如:
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: 10
anchors.leftMargin: 10
应该改写成:
anchors {
top: parent.to
left: parent.left
topMargin: 10
leftMargin: 10
}
(5) qml 组件内容定义顺序规则
qml 组件内容的定义顺序建议如下:
- id 一般可以使用文件名去掉开头的字符 C 作为 id 的值,以小写字母开头,遵循驼峰命名法
- 自定义属性 property
- 信号 signal
- JavaScript functions
- QtObject 封装的私有内容
- 自身的其他属性设置
- 其他子组件
- 状态机(states)
- 动画(animation)
- 转换属性(transitions)
为了更好的可读性,我们要使用空行分隔这些不同的部分
(6) JavaScript 代码规则
- 如果脚本是一个单一的表达式,我们建议写它内联:
Rectangle { color: "blue"; width: parent.width / 3 }
- 如果脚本只有几行,我们通常使用一个块:
Rectangle {
color: "blue"
width: {
var w = parent.width / 3
console.debug(w)
return w
}
}
- 如果脚本超过几行或可以被不同的对象使用,我们建议创建一个函数并像这样调用它:
function calculateWidth(object)
{
var w = object.width / 3
// ...
// more javascript code
// ...
console.debug(w)
return w
}
Rectangle { color: "blue"; width: calculateWidth(parent) }
- 对于长脚本,我们可以把函数放在自己的 JavaScript 文件中,然后像这样导入:
import "myscript.js" as Script
Rectangle { color: "blue"; width: Script.calculateWidth(parent) }
- 如果代码超过一行,因此在一个块中,我们使用分号来表示每个语句的结尾:
MouseArea {
anchors.fill: parent
onClicked: {
var scenePos = mapToItem(null, mouseX, mouseY);
console.log("MouseArea was clicked at scene pos " + scenePos);
}
}
(7) qml 组件内容的命名规则
- qml 组件内容的命名规则应与 C++/Qt 的名称规则一致
3. QML 注释规则
(1) 组件功能注释
/*!
\qmltype Button
\inqmlmodule QtQuick.Controls
\since 5.1
\ingroup controls
\brief A push button with a text label.
\image button.png
The push button is perhaps the most commonly used widget in any graphical
user interface. Pushing (or clicking) a button commands the computer to
perform some action or answer a question. Common examples of buttons are
OK, Apply, Cancel, Close, Yes, No, and Help buttons.
\qml
Button {
text: "Button"
}
\endqml
Button is similar to the QPushButton widget.
You can create a custom appearance for a Button by
assigning a \l {ButtonStyle}.
*/
组件功能注释以“/*!”开始,以“ */” 结束,包含的对文件功能的说明,和控件的使用方法,使用用例以“\qml”开始,以“\endqml”结束
(2) 自定义属性、信号、方法注释
自定义的属性、信号、方法如果仅通过名称不便于理解其意义,就要添加注释,注释示例:
/*! This property holds whether the push button is the default button.
Default buttons decide what happens when the user presses enter in a
dialog without giving a button explicit focus. \note This property only
changes the appearance of the button. The expected behavior needs to be
implemented by the user.
The default value is \c false.
*/
property bool isDefault: false
/*! Assign a \l Menu to this property to get a pull-down menu button.
The default value is \c null.
*/
property Menu menu: null
/*!
\qmlmethod TableViewColumn BasicTableView::addColumn(object column)
Adds a \a column and returns the added column.
The \a column argument can be an instance of TableViewColumn,
or a Component. The component has to contain a TableViewColumn.
Otherwise \c null is returned.
*/
function addColumn(column) {
return insertColumn(columnCount, column)
}
/*!
\qmlsignal TextArea::linkHovered(string link)
\since QtQuick.Controls 1.1
This signal is emitted when the user hovers a link embedded in the text.
The link must be in rich text or HTML format and the
\a link string provides access to the particular link.
\sa hoveredLink
The corresponding handler is \c onLinkHovered.
*/
signal linkHovered(string link)
(3) 其他注释
代码注释格式为:“ // xxxxxxxxx ”,写在被注释代码上方,例如:
function syncHandlesWithSelection()
{
if (!blockRecursion && selectionHandle.delegate) {
blockRecursion = true
// We cannot use property selectionPosition since it gets updated after onSelectionStartChanged
cursorHandle.position = cursorPosition
selectionHandle.position = (selectionStart !== cursorPosition) ? selectionStart : selectionEnd
blockRecursion = false
}
ensureVisible(cursorRectangle)
}
4. QML 工程目录结构规划规则
qml.qrc
|--- main.qml
|--- components ///< 用来放置项目或插件的专有控件
|--- images ///< 用来放置项目或插件的图片资源
|--- views ///< 用来放置用来放置除专有控件、图片资源和 main.qml 外的其他文件
六. Qt 官方编码规范
1. 对齐处理时需要注意
-
当一个指针被强制转换为目标指针类型所需的对齐方式并需要增加位数时,转换后的指针可能在某些目标机上产生运行时崩溃。例如,如果一个 const char * 被转换成一个 const int *,它将在整数必须在两个或四个字节的边界上对齐的机器上崩溃。
-
使用 union 可以强制编译器正确地对齐变量。在下面的示例中,可以确保 AlignHelper 的所有实例都在整数边界处对齐。
union AlignHelper {
char c;
int i;
};
2. 全局对象定义的注意事项
任何有构造函数或需要运行特定代码来初始化的东西都不能在库代码中用作全局对象,因为构造函数/代码将在什么时候运行(第一次使用时、在库加载时、在main()之前或根本不运行时)是未定义的。即使初始化器的执行时间是为共享库定义的,当你在插件中移动代码或者库是静态编译的时候,你也会遇到麻烦:
// global scope
static const QString x; // Wrong - default constructor needs to be run to initialize x
static const QString y = "Hello"; // Wrong - constructor that takes a const char * has to be run
QString z; // super wrong
static const int i = foo(); // wrong - call time of foo() undefined, might not be called at all
我们可以通过如下方式完成上述定义:
// global scope
static const char x[] = "someText"; // Works - no constructor must be run, x set at compile time
static int y = 7; // Works - y will be set at compile time
static MyStruct s = {1, 2, 3}; // Works - will be initialized statically, no code being run
static QString *ptr = nullptr; // Pointers to objects are ok - no code needed to be run to initialize ptr
3. 使用 Q_GLOBAL_STATIC 和 Q_GLOBAL_STATIC_WITH_ARGS 宏来创建静态全局对象
Q_GLOBAL_STATIC(QString, s)
void foo()
{
s()->append("moo");
}
上述代码在 C++11 之后是不会有作用域问题的,在首次进入作用域,构造函数将运行,上述代码是可重入的。
4. char 的默认情况下是有无符号是平台相关的
如果我们确实想要一个 有/无符号的 char,使用 signed char 或 unsigned char 是明智的。在默认 char 是无符号的平台上,以下代码中的条件始终为true:
char c; // c can't be negative if it is unsigned
/********/
/*******/
if (c > 0) { … } // WRONG - condition is always true on platforms where the default is unsigned
5. 避免 64 位 enum 值
- aapcs 将所有 enum 值硬编码为一个 32 位整数
- Microsoft 编译器不支持 64 位 enum 值
6. 使用 enum 来定义常量,不要使用静态 const int 或 defines
- enum 值将在编译时被编译器替换,从而加快代码的速度
- defines 不是名称空间安全的(而且看起来弱爆了)
7. 参数名不要使用简写
- 大多数 ide 会在它们的补全框中显示参数名
- 在文档中看起来更方便
- 错误示范:doSomething(QRegion rgn, QPoint p); 赶紧用doSomething(QRegion clientRegion, QPoint gravitySource) 替换掉前面那段垃圾代码
8. 重新实现一个虚方法时,不要加 'virtual' 关键字了
- 在 Qt5 中,一般使用 override 关键字在函数声明之后“;”(或“{”)之前注释,来标记它们
9. 不从模板/工具类继承
- 析构函数不是虚拟的,这会导致潜在的内存泄漏
- 这些符号没有被导出(大部分是内联的),可能导致莫名其妙的符号冲突
- 例如: 库A有:
库B有:class Q_EXPORT X: public QList<QVariant> {};
突然,QList 的符号从两个库中导出,(crash...)class Q_EXPORT Y: public QList<QVariant> {};
10. 不要混合使用 const 和非 const 迭代器
这可能导致静悄悄的崩溃。
for (Container::const_iterator it = c.begin(); it != c.end(); ++it) //W R O N G
for (Container::const_iterator it = c.cbegin(); it != c.cend(); ++it) // Right
11. Q[Core]Application 是一个单例类
一次只能有一个实例。但是,可以销毁该实例并创建一个新实例,这很可能在ActiveQt或浏览器插件中实现。下面这样的代码很容易崩溃:
static QObject *obj = nullptr;
if (!obj)
obj = new QObject(QCoreApplication::instance());
如果 QCoreApplication 应用程序被销毁,obj 将是一个悬浮指针。应该使用 将是一个悬浮指针。对静态全局对象使用 Q_GLOBAL_STATIC 定义静态全局对象,并使用 qAddPostRoutine 方法来对静态全局对象进行清理。
12. 二进制和源码兼容性
(1) 定义
- Qt 5.0.0 是一个主要版本,Qt 5.1.0 是一个次要版本,Qt 5.1.1 是一个补丁版本
- 向后二进制兼容性:链接到库的早期版本的代码可以继续工作
- 正向二进制兼容性:链接到新版本库的代码与旧版本库兼容
- 源码兼容性:无需修改即可编译代码
在次要版本中保持向后二进制兼容性 + 向后源代码兼容性
在补丁版本中保持向后和向前的二进制兼容性 + 向前和向后的源代码兼容性
- 不要添加/删除任何公共API (例如全局函数、公共/受保护/私有方法)
- 不要重新实现方法(即使是内联方法,也不要实现受保护/私有方法)
- 参考二进制兼容性的变通方法,以保持二进制兼容性
关于二进制兼容性的详细信息:https://community.kde.org/Policies/Binary_Compatibility_Issues_With_C++
在编写 QWidget 子类时,重新实现 event(),即使它是空的。这确保 Widget 子类可以在不破坏二进制兼容性的情况下得到修复。
13. 运算符
一个对两个参数都一视同仁的二元操作符不应该是成员。因为,除了stack overflow 答案中提到的原因外,当操作符是成员时,参数是不相等的。
QLineF 的操作符 == 是一个成员,就是一个很不幸的例子:
QLineF lineF;
QLine lineN;
if (lineF == lineN) // Ok, lineN 可以隐式转换为 QLineF 类型
if (lineN == lineF) // Error: QLineF 无法转为 QLine,且 LHS 是成员无法转换
如果操作符 == 是非成员的方式实现的,则转换规则对两边都适用。
14. 公共头文件的约定
我们的公共头文件必须经受住一些用户的严格设置。所有安装的头文件必须遵循以下规则:
- 没有C风格的类型转换
- 不进行浮点类型的比较
- 使用 qFuzzyCompare 比较两个浮点数是否相等
- 使用 qIsNull 判断一个浮点数是否完全等于 0,而不是将浮点数与 0.0 比较
- 不要在子类中隐藏虚方法
- 如果基类 A 有一个虚的 int val() 方法,而子类 B 有一个同名的重载 int val(int x) 方法,那么 A 的 val 函数就被隐藏了。此时应使用 using 关键字使它再次可见:
class B: public A
{
using A::val;
int val(int x);
};
- 避免同名变量
- 举个例子,就是要避免这种形式: this->x = x;
- 不要给变量与类中声明的函数的参数相同的名称
- 在使用预处理器变量的值之前,一定要检查它是否已定义
#if Foo == 0 // W R O N G
#if defined(Foo) && (Foo == 0) // Right
#if Foo - 0 == 0 // 很棒,但不是谁都明白这是要干啥,为更好的可读性,请使用上面的方法
15. C++ 11 的使用惯例
(1) Lambda 表达式
我们可以在以下限制下使用 lambda 表达式:
- 如果使用 lambda 所在类的静态函数,这样就不要使用 lambda 了,最好改写下代码。例如:
void Foo::something()
{
//...
std::generate(begin, end, []() { return Foo::someStaticFunction(); });
//...
}
我们可以简单地传递函数指针:
void Foo::something()
{
//...
std::generate(begin, end, &Foo::someStaticFunction);
//...
}
原因是 GCC 4.7 和更早的版本有一个 bug,解决它需要捕获 this,但是 Clang 5.0 和更晚的版本会因此产生一个警告:
void Foo::something()
{
// ...
std::generate(begin, end, [this]() { return Foo::someStaticFunction(); });
// warning: lambda capture 'this' is not used [-Wunused-lambda-capture]
...
}
根据以下规则格式化 lambda:
- 始终为参数列表编写括号,即使函数不接受参数。
[]() { doSomething(); }
而不是
[]() { doSomething(); }
- 将捕获列表、参数列表、返回类型和左大括号放在第一行,正文缩进到下面几行,右大括号放在新行。
[]() -> bool {
something();
return isSomethingElse();
}
而不是
[]() -> bool { something();
somethingElse(); }
- 将封闭函数调用的右括号和分号放在与 lambda 的右括号相同的行上。
foo([]() {
something();
});
- 如果在 if 语句中使用 lambda,请在新行中开始 lambda,以避免 lambda 的左大括号和 if 语句的左大括号之间的混淆。
if (anyOf(fooList,
[](Foo foo) {
return foo.isGreat();
})) {
return;
}
而不是
if (anyOf(fooList, [](Foo foo) {
return foo.isGreat();
})) {
return;
}
- 可选的规则,将 lambda 完全放在一行中(如果合适的话)。
foo([]() { return true; });
if (foo([]() { return true; })) {
// ...
}
(2) auto 关键字
可以在以下情况下使用 auto 关键字。如果犹豫要不要使用,例如如果使用 auto 会降低代码的可读性,那么就不要使用 auto。请记住,代码被阅读的次数要比被书写的次数多得多。
- 避免在同一语句中重复书写某个类型时
auto something = new MyCustomType;
auto keyEvent = static_cast<QKeyEvent *>(event);
auto myList = QStringList() << QLatin1String("FooThing") << QLatin1String("BarThing");
- 分配迭代器类型时
auto it = myList.const_iterator();
网友评论