Qt 拾遺 010 Qt/QML 編碼規(guī)范

Qt/QML 編碼規(guī)范

一. 概述

良好的編碼規(guī)范可以大幅提高程序的可讀和可理解性,最終目標是實現(xiàn)更友好的可維護性。

保持良好的開發(fā)和編碼規(guī)范也是一種利己利人的高效習慣,值得每一位開發(fā)者去鍛煉、養(yǎng)成和堅持。

本規(guī)范僅是建議,不能強求一律,允許權(quán)衡例外。

All for one, one for all —— 人人為我,我為人人。合作之道,就在其中。

二. C++ 相關(guān)的約定

  • 不要使用異常
  • 不要使用 rtti (運行時類型信息;即typeinfo結(jié)構(gòu)、dynamic_cast或typeid操作符,包括拋出異常)
  • 明智地使用模板,而不是因為你可以這樣做。(Use templates wisely, not just because you can.)

三. Qt/QML 統(tǒng)一規(guī)范

  1. 行尾不能有空格
  2. 每個 QObject 子類都必須加上 Q_OBJECT 宏,即使它沒有定義任何信號或插槽,否則使用 qobject_cast 將失敗,此時倒不如不繼承 QObject
  3. 你可以將本文檔真的 <SomeOne> 替換為你的公司名稱

四. Qt 編碼規(guī)范

1. 頭文件

(1) 頭文件保護

所有頭文件都應(yīng)該使用 #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 系統(tǒng)可以使用命令行工具,Windows 系統(tǒng)可以使用uuid在線生成工具。

關(guān)于頭文件保護的涉及到的宏的簡介,可以參考《C++ 頭文件保護

(2) 頭文件依賴

減少頭文件中 #include 文件的數(shù)量,盡量使用前向聲明:

# include<MustIncludeClassA>
//...
class SomeClassIncludeInCppA;
class SomeClassIncludeInCppB;
// ...
class MyClass {
// ...
};

(3) 頭文件包含次序

將包含次序標準化可增強可讀性,次序建議如下:

Qt 庫的頭文件、C/C++ 標準庫頭文件、其他三方庫的頭文件、項目內(nèi)定義的頭文件。

如果不得不包含私有頭文件。使用以下語法,不管 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) 變量命名

  1. 變量名是名詞
  2. 首個單詞以小寫字母開頭,后續(xù)單詞以大寫字母開頭
  3. 每個變量占一行
  4. 單一字符的變量只在臨時變量或循環(huán)計數(shù)中使用
  5. 類成員變量需在變量名前加 m_ 前綴,例如:
int m_myValue;
  1. 使用內(nèi)部封裝類作為 d 指針成員變量時,封裝類內(nèi)可不使用 m_ 前綴
  2. 局部變量要等到需要使用時再進行定義變量,且定義時必須進行初始化:整數(shù)用 0、實數(shù)用 0.0、指針用 nullptr、字符用 '\0'
  3. 盡量不要使用全局變量,以降低耦合
  4. bool 類型的變量盡量使用 enabled、visible、movable 這種形式

(5) 常量命名

常量不含前綴且應(yīng)該大寫,單詞間有下劃線,包括全局常量和宏定義,例如:

const int MY_DEFINE_NUMBER = 0;
//...
#define MY_DEFINE_VALUE 0

(6) 函數(shù)命名

函數(shù)命名力求通過名稱就能達到使調(diào)用者知道其返回的是什么屬性值,或者其調(diào)用之后的作用的目標。

因此,取值函數(shù)盡量使用名詞或者帶修飾詞的名詞,而賦值或者帶來改變的函數(shù)采用動賓語法,例如 setWidgetMovable(),返回 bool 的函數(shù)可以使用 is 加屬性名的方式,首個單詞以小寫字母開頭,后續(xù)單詞以大寫字母開頭,例如:

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);

函數(shù)參數(shù)使用名詞或者帶限定修飾詞的名詞,盡量避免使用縮寫,或者使用與 Qt 一致的縮寫形式,首個單詞以小寫字母開頭,后續(xù)單詞以大寫字母開頭,參數(shù)類型如果不是基本數(shù)據(jù)類型,使用 const 引用作為參數(shù),例如:

void drawRectangle(const QPoint& topLeft, const QPoint&  bottomRight);
void drawRectangle(const QRect& rect); 

函數(shù)及其參數(shù)的命名一定不要出現(xiàn)無意義或者根據(jù)其名稱難以知道其作用的名稱,例如:fun0、f1 、param0、p1 等名稱。這種名稱往往作者本人回頭看代碼時都要看下函數(shù)實現(xiàn),才能知道當初自己想要做什么。

(7) 枚舉命名

枚舉名和枚舉值都是名詞,每個單詞以大寫字母開頭,且枚舉名的第一個單詞是 <SomeOne>,例如:

enum SomeOneTitleBarBackgroundColor {
    Transparent,
    White,
    Black,
    Red
};

(8) 命名空間命名

命名空間的名稱是名詞,每個單詞以大寫字母開頭,且前兩個單詞是 <SomeOne>,命名空間的結(jié)束大括號之后跟命名空間名稱的注釋,例如:

namespace SomeOneUtils {
    //...
} // SomeOneUtils

(9) 結(jié)構(gòu)體命名

  1. 結(jié)構(gòu)體中只定義變量,不定義函數(shù),需要定義函數(shù)請使用類
  2. 結(jié)構(gòu)體名使用名詞,每個單詞以大寫字母開頭
  3. 結(jié)構(gòu)體成員使用名詞定義,首單詞以小寫字母開頭,后續(xù)單詞以大寫字母開頭
    示例如下:
struct SuperDevice {
    bool isSuperDevice;
    QString name;
};

3. 注釋約定

(1) 簡單注釋

  1. 單行注釋:/// 或者 //!
  2. 多行注釋:/** 或者 /*!
  3. 注釋掉的代碼使用://
  4. 行內(nèi)注釋掉代碼:/* 和 */

(2) 類注釋

類的頭文件頂部需添加說明性注釋,包括版權(quán)說明、版本號、作者、生成日期、類的功能描述等。

/**
* 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) 常量、變量的注釋

通常其命名時應(yīng)做到足以說明用途,特定情況下,需要額外注釋說明的話,一般可分兩種形式,可以根據(jù)喜好選擇,只要保證在整個工程中統(tǒng)一即可

  1. 代碼上一行注釋
//! 緩存大小
#define BUF_SIZE 1024*4
  1. 代碼后注釋
#define  SIMPLE_PI 3.14  ///< 不精確的圓周率

(4) 函數(shù)注釋

重要函數(shù)頭部應(yīng)該進行注釋,包括函數(shù)名、函數(shù)功能描述、輸入?yún)?shù)、輸出參數(shù)、返回值及其他。以 main 函數(shù)為例:

/**
*
* @brief: 主函數(shù)
* @detail: 應(yīng)用程序入口
* @param argc: 命令參數(shù)個數(shù)
* @param argv: 命令參數(shù)指針數(shù)組
* @return: 程序執(zhí)行成功與否
*      @retval 0: 程序執(zhí)行成功
*      @retval 1: 程序執(zhí)行失敗
* @note: 測試
* @attention:  注意
* @warning: 警告
* @exception: 異常
*/
int main(int argc, char* argv[]) 
{
// ...
    return 0;
// ...
    return 1;
}

(5) 代碼塊或代碼行的注釋

對于實現(xiàn)代碼中重要或不容易理解的地方加以注釋。建議采用代碼塊上方注釋的形式:

 //! 不使用中間變量實現(xiàn)交換變量的值,使用引用或指針都可以
void swap(int& a, int& b)  {  
    a=a^b;  
    b=a^b;  
    a=a^b;  
}  

(6) TODO 注釋

計劃中但未完成的代碼使用TODO注釋,但是一定要對 TODO 的內(nèi)容進行簡單的說明,例如:

 //! 不使用中間變量實現(xiàn)交換變量的值,使用引用或指針都可以
void swap(int& a, int& b)  {  
    a=a^b;  
    b=a^b;  
    a=a^b;  
} 
// TODO: 添加使用中間變量實現(xiàn)交換變量的值的實現(xiàn)版本

(7) doxygen 其他的一些標注方式及說明

命令 生成字段名 備注
@see 參考
@class 引用類 用于文檔生成連接
@var 引用變量 用于文檔生成連接
@enum 引用枚舉 用于文檔生成連接
@code 代碼塊開始 與@endcode成對使用
@endcode 代碼塊結(jié)束 與@code成對使用
@bug 缺陷 鏈接到所有缺陷匯總的缺陷列表
@todo TODO 鏈接到所有TODO 匯總的TODO 列表
@example 使用例子說明
@remarks 備注說明
@pre 函數(shù)前置條件
@deprecated 函數(shù)過時說明

4. 代碼排版約定

(1) 每一行的長度

行長度限定不超過 80 個字符,超出部分分成多行書寫,長表達式要在較低優(yōu)先級操作符處劃分新行,操作符放在新行之首,逗號放在一行的結(jié)束,劃分出的新行要進行適當?shù)目s進,使排版整齊,語句可讀,例如:

 // Wrong
 if (longExpression +
     otherLongExpression +
     otherOtherLongExpression) {
 }

 // Correct
 if (longExpression
     + otherLongExpression
     + otherOtherLongExpression) {
 }

(2) 縮進

使用 4 個空格進行代碼縮進,不要用 Tab 鍵。但是對于由開發(fā)工具自動生成的代碼可以有不一致。

預處理指令不要縮進,從頂格開始,例如:

if (isActive()) {
#if OS_WIN32
    dropEverything();
#endif
    backToNormal();
}

(3) 空白

  • 空行可將語句進行適當?shù)姆纸M,便于閱讀,在相對獨立的代碼塊之間必須加一行空行,且始終只使用一行空行。

  • 一定要在關(guān)鍵字之后和花括號之前使用單個空格:

 // Wrong
 if(foo){
 }

 // Correct
 if (foo) {
 }
  • 對于指針或引用,總是在類型和'*'或'&'之間使用一個空格,但是在'*'或'&'和變量名之間不使用空格:
 char *x;
 const QString &myString;
 const char * const y = "hello";
  • 用空格包圍二進制運算符
  • 在每個逗號后面留一個空格
  • 用于轉(zhuǎn)換的右尖括號后不要有空格(避免C風格的轉(zhuǎn)換)
 // 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) 大括號

  • 類、類方法的實現(xiàn)、全局方法的左大括號永遠單獨占一行,其他情況不單獨占一行,跟在語句后面,例如:

 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
  • 條件語句即使只有一條主體內(nèi)容,也建議使用大括號,以便可能的添加和閱讀
  • 即使條件語句的主體為空時,也使用大括號
 // Wrong
 while (a);

 // Correct
 while (a) {}

(5) 圓括號

使用圓括號將表達式分組,即使運算符的優(yōu)先級相同,或者明確知道表達式的執(zhí)行優(yōu)先級,也要用圓括號進行分組,以便于閱讀,例如:

 // Wrong
 if (a && b || c)

 // Correct
 if ((a && b) || c)

 // Wrong
 a + b & c

 // Correct
 (a + b) & c

(6) switch 語句

  • case 和 switch 位于同一列
  • 每個 case 必須在結(jié)尾有一個 break(或 return)語句,或者使用 Q_FALLTHROUGH() 來表明沒有故意中斷,除非另一個 case 緊隨其后
 switch (myEnum) {
 case Value1:
   doSomething();
   break;
 case Value2:
 case Value3:
   doSomethingElse();
   Q_FALLTHROUGH();
 default:
   defaultHandling();
   break;
 }

5. 代碼編寫上的約定

  1. 代碼采用 C++11 標準,盡量使用智能指針,盡量不使用裸指針(Qt 中使用 QScopedPointer)
  2. 使用 Qt5 新的信號和槽連接方式,信號和槽的命名不加 signal 和 slot 前綴,用動作和on動作方式,如信號 clicked(),槽為 onClicked()
  3. 關(guān)鍵代碼需要有單元測試,非界面代碼一般認為是關(guān)鍵代碼
  4. 默認debug build前推薦使用靜態(tài)代碼檢查工具(Cppcheck/Clang/VS)檢查,編譯時 使用 gcc sanitizer 增加地址和未定義行為動態(tài)檢查
  5. 開啟 qtcreator 插件 beautifier,并配置 clang-format 格式化工具,進行項目代碼整體風格的統(tǒng)一管理,可以參考使用 Qt Creator 插件 beautifier 配置 clang-format 工具管理項目代碼整體風格
  6. 在填入界面顯示文字時,使用英文,通過 Qt Linguist 翻譯成中文,以便多語言的實現(xiàn),文件編碼一律使用 UTF-8,添加 BOM
  7. 在繪制界面的時候,盡量使用 layout 和 stretch 來布局,盡量不要使用固定的大小,例如固定 10 像素
  8. 避免使用 C 類型的強制轉(zhuǎn)換,選擇 C++ 類型的強制轉(zhuǎn)換(static_cast、const_cast、reinterpret_cast),雖說 reinterpret_cast 和 C 類型的強制轉(zhuǎn)換都是危險的,但至少 reinterpret_cast 不會刪除 const 修飾符
  9. 使用 qobject_cast 來代替 dynamic_cast
  10. 使用構(gòu)造函數(shù)來轉(zhuǎn)換簡單類型: int(myFloat) 而不是 (int)myFloat

五. QML 編碼規(guī)范

1. 文件命名

(1) QML 文件命名

  1. 僅由英文單詞組成
  2. 以大寫字母 C (Custom type 之意)開始,遵循大駝峰法的命名規(guī)則
    示例:
CProgressDialog.qml
CListView.qml
CMainWindow.qml

(2) js 文件命名

  1. js 文件名由小寫的英文單詞和 _ 連接組成
  2. js 文件名一律以“ <someone> ”結(jié)尾(<someone>表示公司或組織名稱)

2. 編碼規(guī)范

(1) import 規(guī)則

qml 文件中,import 模塊的建議順序為:

  1. Qt 內(nèi)置模塊
  2. 框架自定義 Qt Quick 模塊
  3. 項目自定義 Qt Quick 模塊
  4. 本地 *.qml 組件包
  5. javascript 導入腳本文件
  6. 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”關(guān)鍵字設(shè)置該模塊在本 qml 文件中的“別名”,對于"as"的相關(guān)規(guī)則:

  1. 導入 Qt 內(nèi)置模塊的版本號選擇對應(yīng)的 Qt 版本的最高版本
  2. 導入 js 文件到 qml 中必須要命名別名
  3. 導入模塊,存在命名沖突時,則至少給其中一個模塊設(shè)置別名
  4. 不對 Qt 內(nèi)置模塊設(shè)置別名
  5. 對于自定義模塊需設(shè)置別名
  6. 別名需保持在當前 qml 文件內(nèi)的唯一性
  7. 別名的命名規(guī)則采用大駝峰法

(2) qml 組件組成結(jié)構(gòu)規(guī)則

qml 文件描述的實際上一個由 “UI組件節(jié)點” 組成的“樹”。而每一個 qml 有有且只有一個“根節(jié)點”。我們可以稱其為“根組件”。
跟組件的定義采用使 qml 文件復雜度最低原則,例如我們有如下 qml 文件:

// CPageBackground.qml
Item {
    // ...
    Rectangle {
        // ...
    }
}

則應(yīng)改寫為:

// CPageBackground.qml
Rectangle {
    // ...
}

除非有不得不封裝的理由,否則應(yīng)以堅持降低 qml 的復雜度為首要原則。

(3) qml 組件封裝的實現(xiàn)規(guī)則

qml 文件的根組件及新定義的屬性、方法、信號,都對其使用者可見,因此按照約定俗成的添加雙下劃線("__")前綴的方式并不能避免用戶有意的對相應(yīng)的屬性、方法、信號的調(diào)用。

因此,本規(guī)則建議將需要作為私有(private)的屬性、方法、信號,包含在 QtObject 節(jié)點組件中。

示例:

// 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 組件內(nèi)容的定義規(guī)則

  1. 一行只寫一條語句
  2. 使用 QML 進行顯示,使用 C++/Qt 處理復雜的計算和數(shù)據(jù)管理邏輯
  3. Loader 出來的 qml 界面,在界面關(guān)閉時,一定要清空 Loader 資源
  4. js 代碼中,能夠用條件運算符(? :)代替 if...else... 地方,要用條件運算符,這樣使代碼更簡潔明了
  5. 一個 qml 文件中,設(shè)置 id 的個數(shù)不要超過 7 個,多余 7 個,則最好進行 qml 代碼的拆分,建議設(shè)置不超過 4 個 id 為宜,更小粒度的 qml 更加便于閱讀和維護
  6. 當一個 qml 文件中,代碼行達到 200 至 300 行時,要從其 qml 文件中分解出子 qml 文件,這樣可以避免出現(xiàn)控件關(guān)聯(lián)復雜,篇幅較長的代碼。原因也是,更小粒度的 qml 更加便于閱讀和維護
  7. 如果有歸屬同一類組的多個屬性需要設(shè)置,為了提高代碼可讀性,則使用組的提示符,而不是點的運算符號
    例如:
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: 10
anchors.leftMargin: 10

應(yīng)該改寫成:

anchors {
    top: parent.to
    left: parent.left
    topMargin: 10
    leftMargin: 10
}

(5) qml 組件內(nèi)容定義順序規(guī)則

qml 組件內(nèi)容的定義順序建議如下:

  1. id 一般可以使用文件名去掉開頭的字符 C 作為 id 的值,以小寫字母開頭,遵循駝峰命名法
  2. 自定義屬性 property
  3. 信號 signal
  4. JavaScript functions
  5. QtObject 封裝的私有內(nèi)容
  6. 自身的其他屬性設(shè)置
  7. 其他子組件
  8. 狀態(tài)機(states)
  9. 動畫(animation)
  10. 轉(zhuǎn)換屬性(transitions)

為了更好的可讀性,我們要使用空行分隔這些不同的部分

(6) JavaScript 代碼規(guī)則

  • 如果腳本是一個單一的表達式,我們建議寫它內(nèi)聯(lián):
Rectangle { color: "blue"; width: parent.width / 3 }
  • 如果腳本只有幾行,我們通常使用一個塊:
Rectangle {
    color: "blue"
    width: {
        var w = parent.width / 3
        console.debug(w)
        return w
    }
}
  • 如果腳本超過幾行或可以被不同的對象使用,我們建議創(chuàng)建一個函數(shù)并像這樣調(diào)用它:
function calculateWidth(object)
{
    var w = object.width / 3
    // ...
    // more javascript code
    // ...
    console.debug(w)
    return w
}

Rectangle { color: "blue"; width: calculateWidth(parent) }
  • 對于長腳本,我們可以把函數(shù)放在自己的 JavaScript 文件中,然后像這樣導入:
import "myscript.js" as Script

Rectangle { color: "blue"; width: Script.calculateWidth(parent) }
  • 如果代碼超過一行,因此在一個塊中,我們使用分號來表示每個語句的結(jié)尾:
MouseArea {
    anchors.fill: parent
    onClicked: {
        var scenePos = mapToItem(null, mouseX, mouseY);
        console.log("MouseArea was clicked at scene pos " + scenePos);
    }
}

(7) qml 組件內(nèi)容的命名規(guī)則

  • qml 組件內(nèi)容的命名規(guī)則應(yīng)與 C++/Qt 的名稱規(guī)則一致

3. QML 注釋規(guī)則

(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}.
*/

組件功能注釋以“/*!”開始,以“ */” 結(jié)束,包含的對文件功能的說明,和控件的使用方法,使用用例以“\qml”開始,以“\endqml”結(jié)束

(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 工程目錄結(jié)構(gòu)規(guī)劃規(guī)則

qml.qrc
|--- main.qml
|--- components ///< 用來放置項目或插件的專有控件
|--- images ///< 用來放置項目或插件的圖片資源
|--- views ///< 用來放置用來放置除專有控件、圖片資源和 main.qml 外的其他文件

六. Qt 官方編碼規(guī)范

1. 對齊處理時需要注意

  • 當一個指針被強制轉(zhuǎn)換為目標指針類型所需的對齊方式并需要增加位數(shù)時,轉(zhuǎn)換后的指針可能在某些目標機上產(chǎn)生運行時崩潰。例如,如果一個 const char * 被轉(zhuǎn)換成一個 const int *,它將在整數(shù)必須在兩個或四個字節(jié)的邊界上對齊的機器上崩潰。

  • 使用 union 可以強制編譯器正確地對齊變量。在下面的示例中,可以確保 AlignHelper 的所有實例都在整數(shù)邊界處對齊。

union AlignHelper {
    char c;
    int i;
};

2. 全局對象定義的注意事項

任何有構(gòu)造函數(shù)或需要運行特定代碼來初始化的東西都不能在庫代碼中用作全局對象,因為構(gòu)造函數(shù)/代碼將在什么時候運行(第一次使用時、在庫加載時、在main()之前或根本不運行時)是未定義的。即使初始化器的執(zhí)行時間是為共享庫定義的,當你在插件中移動代碼或者庫是靜態(tài)編譯的時候,你也會遇到麻煩:

// 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 宏來創(chuàng)建靜態(tài)全局對象

Q_GLOBAL_STATIC(QString, s)

void foo()
{
    s()->append("moo");
}

上述代碼在 C++11 之后是不會有作用域問題的,在首次進入作用域,構(gòu)造函數(shù)將運行,上述代碼是可重入的。

4. char 的默認情況下是有無符號是平臺相關(guān)的

如果我們確實想要一個 有/無符號的 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 位整數(shù)
  • Microsoft 編譯器不支持 64 位 enum 值

6. 使用 enum 來定義常量,不要使用靜態(tài) const int 或 defines

  • enum 值將在編譯時被編譯器替換,從而加快代碼的速度
  • defines 不是名稱空間安全的(而且看起來弱爆了)

7. 參數(shù)名不要使用簡寫

  • 大多數(shù) ide 會在它們的補全框中顯示參數(shù)名
  • 在文檔中看起來更方便
  • 錯誤示范:doSomething(QRegion rgn, QPoint p); 趕緊用doSomething(QRegion clientRegion, QPoint gravitySource) 替換掉前面那段垃圾代碼

8. 重新實現(xiàn)一個虛方法時,不要加 'virtual' 關(guān)鍵字了

  • 在 Qt5 中,一般使用 override 關(guān)鍵字在函數(shù)聲明之后“;”(或“{”)之前注釋,來標記它們

9. 不從模板/工具類繼承

  • 析構(gòu)函數(shù)不是虛擬的,這會導致潛在的內(nèi)存泄漏
  • 這些符號沒有被導出(大部分是內(nèi)聯(lián)的),可能導致莫名其妙的符號沖突
  • 例如: 庫A有:
    class Q_EXPORT X: public QList<QVariant> {};
    
    庫B有:
    class Q_EXPORT Y: public QList<QVariant> {};
    
    突然,QList 的符號從兩個庫中導出,(crash...)

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 是一個單例類

一次只能有一個實例。但是,可以銷毀該實例并創(chuàng)建一個新實例,這很可能在ActiveQt或瀏覽器插件中實現(xiàn)。下面這樣的代碼很容易崩潰:

static QObject *obj = nullptr;
if (!obj)
    obj = new QObject(QCoreApplication::instance());

如果 QCoreApplication 應(yīng)用程序被銷毀,obj 將是一個懸浮指針。應(yīng)該使用 將是一個懸浮指針。對靜態(tài)全局對象使用 Q_GLOBAL_STATIC 定義靜態(tài)全局對象,并使用 qAddPostRoutine 方法來對靜態(tài)全局對象進行清理。

12. 二進制和源碼兼容性

(1) 定義

  • Qt 5.0.0 是一個主要版本,Qt 5.1.0 是一個次要版本,Qt 5.1.1 是一個補丁版本
  • 向后二進制兼容性:鏈接到庫的早期版本的代碼可以繼續(xù)工作
  • 正向二進制兼容性:鏈接到新版本庫的代碼與舊版本庫兼容
  • 源碼兼容性:無需修改即可編譯代碼

在次要版本中保持向后二進制兼容性 + 向后源代碼兼容性

在補丁版本中保持向后和向前的二進制兼容性 + 向前和向后的源代碼兼容性

  • 不要添加/刪除任何公共API (例如全局函數(shù)、公共/受保護/私有方法)
  • 不要重新實現(xiàn)方法(即使是內(nèi)聯(lián)方法,也不要實現(xiàn)受保護/私有方法)
  • 參考二進制兼容性的變通方法,以保持二進制兼容性

關(guān)于二進制兼容性的詳細信息:https://community.kde.org/Policies/Binary_Compatibility_Issues_With_C++

在編寫 QWidget 子類時,重新實現(xiàn) event(),即使它是空的。這確保 Widget 子類可以在不破壞二進制兼容性的情況下得到修復。

13. 運算符

在成員與非成員之間抉擇

一個對兩個參數(shù)都一視同仁的二元操作符不應(yīng)該是成員。因為,除了stack overflow 答案中提到的原因外,當操作符是成員時,參數(shù)是不相等的。

QLineF 的操作符 == 是一個成員,就是一個很不幸的例子:

QLineF lineF;
QLine lineN;

if (lineF == lineN) // Ok,  lineN 可以隱式轉(zhuǎn)換為 QLineF 類型
if (lineN == lineF) // Error: QLineF 無法轉(zhuǎn)為 QLine,且 LHS 是成員無法轉(zhuǎn)換

如果操作符 == 是非成員的方式實現(xiàn)的,則轉(zhuǎn)換規(guī)則對兩邊都適用。

14. 公共頭文件的約定

我們的公共頭文件必須經(jīng)受住一些用戶的嚴格設(shè)置。所有安裝的頭文件必須遵循以下規(guī)則:

  • 沒有C風格的類型轉(zhuǎn)換
  • 不進行浮點類型的比較
    • 使用 qFuzzyCompare 比較兩個浮點數(shù)是否相等
    • 使用 qIsNull 判斷一個浮點數(shù)是否完全等于 0,而不是將浮點數(shù)與 0.0 比較
  • 不要在子類中隱藏虛方法
    • 如果基類 A 有一個虛的 int val() 方法,而子類 B 有一個同名的重載 int val(int x) 方法,那么 A 的 val 函數(shù)就被隱藏了。此時應(yīng)使用 using 關(guān)鍵字使它再次可見:
  class B: public A
{
    using A::val;
    int val(int x);
};
  • 避免同名變量
    • 舉個例子,就是要避免這種形式: this->x = x;
    • 不要給變量與類中聲明的函數(shù)的參數(shù)相同的名稱
  • 在使用預處理器變量的值之前,一定要檢查它是否已定義
#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 所在類的靜態(tài)函數(shù),這樣就不要使用 lambda 了,最好改寫下代碼。例如:
void Foo::something()
{
    //...
    std::generate(begin, end, []() { return Foo::someStaticFunction(); });
    //...
}

我們可以簡單地傳遞函數(shù)指針:

void Foo::something()
{
    //...
    std::generate(begin, end, &Foo::someStaticFunction);
    //...
}

原因是 GCC 4.7 和更早的版本有一個 bug,解決它需要捕獲 this,但是 Clang 5.0 和更晚的版本會因此產(chǎn)生一個警告:

void Foo::something()
{
    // ...
    std::generate(begin, end, [this]() { return Foo::someStaticFunction(); });
    // warning: lambda capture 'this' is not used [-Wunused-lambda-capture]
    ...
}

根據(jù)以下規(guī)則格式化 lambda:

  • 始終為參數(shù)列表編寫括號,即使函數(shù)不接受參數(shù)。
[]() { doSomething(); }

而不是

[]() { doSomething(); }
  • 將捕獲列表、參數(shù)列表、返回類型和左大括號放在第一行,正文縮進到下面幾行,右大括號放在新行。
[]() -> bool {
    something();
    return isSomethingElse();
}

而不是

[]() -> bool { something();
    somethingElse(); }
  • 將封閉函數(shù)調(diào)用的右括號和分號放在與 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;
}
  • 可選的規(guī)則,將 lambda 完全放在一行中(如果合適的話)。
foo([]() { return true; });

if (foo([]() { return true; })) {
    // ...
}

(2) auto 關(guān)鍵字

可以在以下情況下使用 auto 關(guān)鍵字。如果猶豫要不要使用,例如如果使用 auto 會降低代碼的可讀性,那么就不要使用 auto。請記住,代碼被閱讀的次數(shù)要比被書寫的次數(shù)多得多。

  • 避免在同一語句中重復書寫某個類型時
auto something = new MyCustomType;
auto keyEvent = static_cast<QKeyEvent *>(event);
auto myList = QStringList() << QLatin1String("FooThing") << QLatin1String("BarThing");
  • 分配迭代器類型時
auto it = myList.const_iterator();

七. 參考鏈接

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容