Qt的編程風格與規范
來源: http://blog.csdn.net/qq_35488967/article/details/70055490
參考資料:
- https://wiki.qt.io/Qt_Contribution_Guidelines
- https://wiki.qt.io/Qt_Coding_Style
- https://wiki.qt.io/Coding_Conventions
- https://community.kde.org/Policies/Library_Code_Policy
- https://wiki.qt.io/UI_Text_Conventions
- https://wiki.qt.io/API_Design_Principles
- http://doc.qt.io/qt-5/qml-codingconventions.html
- https://google.github.io/styleguide/cppguide.html
變量聲明
- 聲明每一個變量都要用獨立的一行
- 避免短的或無意義的命名
- 單個字符的變量名只適用於用來計數的臨時變量,因為此時該變量的用途十分明顯
- 當一個變量被用到時再聲明它
// Wrong int a, b; char *c, *d; // Correct int height; int width; char *nameOfThis; char *nameOfThat;
變量一般命名法
- 變量名和函數名以小寫字母開頭,開頭之后的部分每個單詞以大寫字母開頭
- 避免使用縮寫
// Wrong short Cntr; char ITEM_DELIM = ' '; // Correct short counter; char itemDelimiter = ' ';
變量在Qt中的命名
- 類名以大寫字母開頭,公開類以Q開頭,緊跟大寫字母;公用函數以q開頭。(此為Qt內部規范,我們可不遵守)
- 首字母縮寫詞出現在命名中,采用駝峰命名法,如QXmlStreamReader,而不是QXMLStreamReader(即只有第一個字母大寫)
空白行與空格的使用
- 用空行在適當的地方划分代碼塊
- 總是只用一個空行
- 在關鍵詞和花括號之間總是只用一個空格符
// Wrong if(foo){ } // Correct if (foo) { }
指針的書寫規范
- 對於指針或引用,在類型名和或&之間用一個空格,但是在或&和變量名之間沒有空格
char *x; const QString &myString; const char * const y = "hello";
二元操作符
- 二元操作符的左右都要有空格
- 二元操作符對待它的兩個參數是同樣對待的,只是在該操作符是類外的操作符
- 例如QLineF有它自己的==操作符
QLineF lineF;
QLine lineN;
if (lineF == lineN) // Ok, lineN is implicitly converted to QLineF if (lineN == lineF) // Error: QLineF cannot be converted implicitly to QLine, and the LHS is a member so no conversion applies
逗號
- 逗號左邊沒有空格,逗號右邊有一個空格
#include <QApplication> #include <QMessageBox> int main(int argc, char *argv[]) { QT_REQUIRE_VERSION(argc, argv, "4.0.2") QApplication app(argc, argv); ... return app.exec(); }
分號
- 分號左邊沒有空格,分號作為語句的結束符,其右邊一般不再有內容
struct Point2D { int x; int y; };
井號
- #號右邊沒有空格
#include <QtGlobal> #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)) #include <QtWidgets> #else #include <QtGui> #endif
引號
- 左引號的左邊和右引號的右邊都有一個空格,左引號的右邊和右引號的左邊都沒有空格
- 如果右引號右邊是又括號的話,它們之間沒有空格
qDebug() << Q_FUNC_INFO << "was called with value1:" << value1 << "value2:" << value2; QT_REQUIRE_VERSION(argc, argv, "4.0.2")
cast
- cast后無須空格
// Wrong char* blockOfMemory = (char* ) malloc(data.size()); // Correct char *blockOfMemory = reinterpret_cast<char *>(malloc(data.size()));
- 避免C語言的casts,盡量用C++的casts (static_cast, const_cast, reinterpret_cast)。 reinterpret_cast 和 C風格的cast用起來都是危險的,但至少 reinterpret_cast 不會把const修飾符去掉。
- 涉及到QObjects或重構自己的代碼時,不要使用dynamic_cast,而是用qobject_cast,例如在引進一個類型的方法時。
- 用構造函數去cast簡單類型,例如:用int(myFloat)代替(int)myFloat
語句
- 不要在一行寫多條語句
- 另起一行寫控制流語句的定義
// Wrong if (foo) bar(); // Correct if (foo) bar();
花括號寫法
- 使用緊貼括號:左括號和語句的開頭在同一行,如果右括號是緊跟在一個關鍵詞之后的,則右括號和該關鍵詞在同一行
// Wrong if (codec) { } else { } // Correct if (codec) { } else { }
- 例外:函數的實現和類的聲明中,左括號總是在一行的開頭
static void foo(int g) { qDebug("foo: %i", g); } class Moo { };
- 當條件語句的執行部分多於一句的時候才使用花括號
// Wrong if (address.isEmpty()) { return false; } for (int i = 0; i < 10; +''i) { qDebug("%i", i); } // Correct if (address.isEmpty()) return false; for (int i = 0; i < 10;i) qDebug("%i", i);
花括號用途
- 例外1:如果父語句占有多行,或經過多層封裝,子語句要用到花括號
// Correct if (address.isEmpty() || !isValid() || !codec) { return false; }
- 例外2:對稱原則:在if-else語句塊中,如果if或else中的一個包含了多行,另一個為了對稱性原則,也要用花括號
// Wrong if (address.isEmpty()) qDebug("empty!"); else { qDebug("%s", qPrintable(address)); it; } // Correct if (address.isEmpty()) { qDebug("empty!"); } else { qDebug("%s", qPrintable(address)); it; } // Wrong if (a) … else if (b) … // Correct if (a) { … } else { if (b) … }
- 當條件語句的執行體是空語句的時候,用一個花括號
// Wrong while (a); // Correct while (a) {}
圓括號
- 圓括號用來給語句分組
// Wrong if (a && b || c) // Correct if ((a && b) || c) // Wrong a + b & c // Correct (a + b) & c
Switch 語句
- case標簽和switch在同一列
- 每一個case語句的末尾都要有一個break語句或return語句,除非因功能需要故意不加或另外一個case是緊跟上一個case的。
switch (myEnum) { case Value1: doSomething(); break; case Value2: case Value3: doSomethingElse(); // fall through default: defaultHandling(); break; }
跳轉語句
- 包括:break, continue, return, and goto
- 不要在跳轉關鍵詞后邊加else
// Wrong if (thisOrThat) return; else somethingElse(); // Correct if (thisOrThat) return; somethingElse();
- 例外:如果這段代碼是固有的對稱結構,用else實現視覺上的對稱也是可以的
換行
- 每行代碼不多於100個字符;若有必要,用括號括起來
- 逗號在行尾。操作符在新行的開頭位置,這是因為編輯器過窄的話,操作符在行尾容易看不見
- 換行時盡量避免行於行之間看起來參差不齊
// Wrong if (longExpression + otherLongExpression + otherOtherLongExpression) { } // Correct if (longExpression + otherLongExpression + otherOtherLongExpression) { }
一般例外與Artistic style選項
- 一般例外:當嚴格遵守一條規范會讓你的代碼看起來很糟糕時,廢棄這條規范
- 用astyle格式化代碼時的選項
--style=kr --indent=spaces=4 --align-pointer=name --align-reference=name --convert-tabs --attach-namespaces --max-code-length=100 --max-instatement-indent=120 --pad-header --pad-oper
- 例如,你可以這樣用以上的代碼
int foo = some_really_long_function_name(and_another_one_to_drive_the_point_home( first_argument, second_argument, third_arugment));
C++特性
- 不要使用異常處理
- 不要使用運行時類型識別
- 理智地使用模板,不要僅僅因為你會用就去用
Qt源碼中的規范
- 所有代碼都是ascii,使用者如果不確定的話,只可能是7字節
- 每一個QObject的子類都必須有Q_OBJECT宏,即使這個類沒用到信號或槽。否則qobject_cast將不能使用
- 在connect語句中,使信號和槽的參數規范化(參看 QMetaObject::normalizedSignature),可以加快信號/槽的查找速度。可以使用qtrepotools/util/normalize規范已有代碼
包含頭文件
- 用#include
#include <qstring.h> // Qt class #include <new> // STL stuff #include <limits.h> // system stuff
- 如果你想包含qplatforms.h,把它作為第一個被包含的頭文件
- 如果你想包含私有頭文件,要十分小心。使用以下的語法而不用管whatever_p.h屬於哪個模塊在哪個文件目錄下
#include <private/whatever_p.h>
編譯器/平台特定問題
- 使用三目運算符 ?時要特別小心,如果每次的返回值的類型可能不一樣的話,一些編譯器會在運行時生成沖突的代碼(此時編譯器甚至不會報錯)
QString s;
return condition ? s : "nothing"; // crash at runtime - QString vs. const char *
- 要特別小心對齊問題。無論何時,當一個指針被cast后的對齊數是增加的時候,它都可能會崩潰。例如一個const char 被cast成了const int,當cast之后的數字不得不在2或4個字節之間對齊時,指針就會在機器上崩潰。
- 使用一個union強迫編譯器正確地對齊變量,示例如下,你可以確定AlignHelper中所有的實例都和int邊界對齊了
union AlignHelper { char c; int i; };
- 任何需要需要執行構造函數或相關代碼進行初始化的實例,都不能用作庫代碼中的全局實例。因為當構造函數或代碼將要運行的時候,該實例還沒有被定義(在第一次調用該實例時,在加載庫時,在執行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 = 0; // Pointers to objects are ok - no code needed to be run to initialize ptr
- 用Q_GLOBAL_STATIC定義全局實例
Q_GLOBAL_STATIC(QString, s)
void foo() { s()->append("moo"); }
- char型變量是有符號的還是無符號的取決於它運行環境的架構。如果你明確地想使用一個signed或unsinged char,就使用signed char或unsigned char。以下代碼運行在把char默認為無符號的平台上時,其條件判斷恆為真。
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
- 避免64位的枚舉值
- 嵌入式應用系統二進制接口將所有的枚舉類型的值硬編碼成32位int值
- 微軟的編譯器不支持64位的枚舉值
編程美學
- 偏愛用枚舉值定義常量而非用const int或defines
- 枚舉值會在編譯時被編譯器用實際值替換掉,因而運行時得出結果的速度更快
- defines不是命名空間安全的(並且看起來很丑)
- 偏愛使用冗長而詳細的參數名
- 當重新實現一個虛方法時,不要在頭文件中用virtual關鍵字,在Qt5中,用 Q_DECL_OVERRIDE宏在函數聲明之后,分號之前注解它。
避免出現的事
- 不要繼承模版/工具類
- 其析構函數不是虛函數,會導致潛在的內存泄漏
- 其符號不是導出的(not exported),會導致符號沖突
// 例如:A庫有以下代碼 class Q_EXPORT X: public QList<QVariant> {};
//B庫有以下代碼 class Q_EXPORT Y: public QList<QVariant> {};
這樣,QList的符號就被導出了兩次
- 不要把const-iterator和none-const iterator搞混。
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
- Q[Core]Application 是一個單例類。同一時間只能有一個實例在運行,但是這個實例可以被銷毀,新的實例將可以被創建,如下的代碼容易產生崩潰
static QObject *obj = 0; if (!obj) obj = new QObject(QCoreApplication::instance());
- 當QCoreApplication application被銷毀時,obj成為了迷途指針(野指針),可以用 Q_GLOBAL_STATIC 和qAddPostRoutine清理
- 為了盡可能地支持靜態關鍵詞,避免使用匿名命名空間。編譯單位內的一個靜態名稱可以保證它是一個內部連接。而一個位於匿名命名空間的名稱,C++規定它是一個外部鏈接。
二進制和源兼容性
- 定義
- Qt 4.0.0是一個主版本,Qt 4.1.0是一個微調版本,Qt 4.1.1是一個補丁版本。
- 在此之后的版本:代碼鏈接到之前版本的庫可以運行
- 在此之前的版本:代碼鏈接到一個新版本的庫只對舊版本的庫能工作。
- 源碼兼容性:源碼在不修改的情況下進行編譯
- 微調版本保持向后的二進制兼容性
- 補丁版本保持向后和向前的二進制兼容性
- 不要增加或去掉任何公共API(例如公共的函數,公有/保護/私有的方法)
- 不要重新實現方法(甚至不要修改內連方法,也不要修改保護/私有方法)
- 當繼承一個QWidget的子類時,總是要去重寫event(),即使它是空的。這將使你的widget類可以被操作而不破壞其二進制兼容性
- 所有從Qt中導出的方法,必須以q或Q開頭。用autotest符號檢測是否存在違反此規則的情況。(此為Qt本身要求的規范,我們不需要嚴格執行。)
命名空間
- 請記住,Qt中,除了Tests和WibKit,全部都是處在命名空間中的代碼
float值
- 沒有float值之間的比較
- 用qFuzzyCompare去和delta比較其值
- 用qIsNull去判斷float值是不是二進制0,而不是和0.0比較。
[static] bool qFuzzyCompare(double p1, double p2) // Instead of comparing with 0.0 qFuzzyCompare(0.0,1.0e-200); // This will return false // Compare adding 1 to both values will fix the problem qFuzzyCompare(1 + 0.0, 1 + 1.0e-200); // This will return true
虛方法
- 不要在子類中隱藏父類的虛方法:假設A類中有個virtual int val()方法,以下代碼是不規范的。
class B: public A { using A::val; int val(int x); };
宏定義
- 在操作一個預處理器之前,先判斷它是否被定義
#if Foo == 0 // W R O N G #if defined(Foo) && (Foo == 0) // Right #if Foo - 0 == 0 // Clever, are we? Use the one above instead, for better readability
類的成員命名
- 成員變量一般為名詞
- 函數成員一般為動詞/動詞+名詞,但是當動詞為get時,get常常省略。當返回值為Bool型變量時,函數名一般以前綴’is’開頭
public: void setColor(const QColor& c); QColor color() const; void setDirty(bool b); bool isDirty() const; private Q_SLOTS: void slotParentChanged();
定義私有類
//.h文件 class KFooPrivate; class KFoo { public: /* public members */ private: const QScopedPointer<KFooPrivate> d; };
//.cpp文件
class KFooPrivate
{
public:
int someInteger;
};
KFoo::KFoo() : d(new KFooPrivate)
{
/* ... */ } KFoo::~KFoo() { // You must define a non-inline destructor in the .cpp file, even if it is empty // else, a default one will be built in placed where KFooPrivate is only forward // declare, leading to error in the destructor of QScopedPointer }
標記(flags)
- 避免使用無意義的bool型參數,以下是糟糕的例子
static QString KApplication::makeStdCaption( const QString &caption, bool withAppName, bool modified);
- 解決方案是用QFlags。即使其中只有一個值,也建議這么做,這將允許你以后很方便地添加更多的值並且保持二進制兼容性。
- 示例如下:
class KApplication
{
public: /* [...] */ enum StandardCaptionOption { /** * Indicates to include the application name */ WithApplicationName = 0x01, /** * Note in the caption that there is unsaved data */ Modified = 0x02 }; Q_DECLARE_FLAGS(StandardCaptionOptions, StandardCaptionOption) /** * Builds a caption using a standard layout. * * @param userCaption The caption string you want to display * @param options a set of flags from MakeStandartCaptionOption */ static QString makeStandardCaption(const QString& userCaption, const StandardCaptionOptions& options = WithApplicationName); /* [...] */ }; Q_DECLARE_OPERATORS_FOR_FLAGS(KApplication::StandardCaptionOptions)
常引用
- 每一個對象,只要它不是基礎類型(int, float, bool, enum, or pointers),都應該以常量引用的形式傳遞。這條使得代碼運行得更快。即使一個對象是隱式共享的,也應該這么做
QString myMethod( const QString& foo, const QPixmap& bar, int number );
- 避免常引用的返回類型
const QList<int> &someProperty() const;
- 有種情況還是可以使用常引用的返回類型的,這種情況下,此處代碼的運行性能至關重要,此處的代碼實現也是固定的,思考再三之后,你可以寫成這樣:
QList<int> someProperty() const;
庫代碼中的信號&槽
- 在庫代碼中,用Q_SIGNALS 和 Q_SLOTS 代替 signals 和 slots。它們在語法上是相等的,用以避免和boost信號的沖突。和python協同工作時使用”slots”
屬性
- 設置屬性時用Q_PROPERTY。理由是屬性可以被javascript 接口訪問到
- moc中設置特定的flag用 QMetaProperty.
構造函數
- 為了使構造函數被錯誤使用的可能性降到最小,每一個構造函數(除了拷貝構函數)都應該檢查自己是否需要加上explicit 符號。
#include
- 盡量減少在頭文件中包含其他頭文件的數量
- 如下所示,可以用前置聲明法
#include <kfoobase.h> class KBar; class KFoo : public KFooBase { public: /* [...] */ void myMethod(const KBar &bar); };
- 包含Qt自帶頭文件或外部庫的頭文件用尖括號
#include <iostream> #include <QDate> #include <zlib.h>
- 包含自己的項目頭文件用雙引號
#include "myclass.h"
- 包含Qt類的頭文件不用包含它所在的模塊名
//正確示例 #include <QDate> //correct
//錯誤示例 #include <QtCore/QDate> //incorrect, no need to specify the module QtCore #include <QtCore> //incorrect, do not include the top-level Qt module
- 假如你有一個Foo類,有Foo.h文件和Foo.cpp文件,在你的Foo.cpp文件中,要先包含Foo.h文件再包含其他頭文件
- 如果你的代碼寫成下面這樣:
//.h文件 class Foo { public: Bar getBar(); };
.cpp文件
#include "bar.h" #include "foo.h"
- 你的cpp文件能夠正常編譯,但當其他人用到Foo.h文件時,如果沒有包含bar.h文件,編譯器將不能進行編譯。
- 因此在cpp文件中首先包含其相應的.h文件,是為了.h文件能夠被其他人使用
- 頭文件必須進行包含保護:避免多次包含而引起多次的編譯
#ifndef MYFOO_H #define MYFOO_H ... <stuff>... #endif /* MYFOO_H */
信號&槽的標准化寫法
- 標准化的寫法增加代碼可讀性
- 不標准的寫法可能是如下寫法
QObject::connect(this, SIGNAL( newValue(const QString&, const MyNamespace::Type&) ), other, SLOT( value(const QString &) ));
- 建議采用以下寫法
QObject::connect(this, SIGNAL(newValue(QString,MyNamespace::Type)), other, SLOT(value(QString)));
API-最小化原則
- 最小化的API意味着,每個API中使用盡可能少的類,每個類中使用盡可能少的公用成員(public members)。這樣做的好處是使得API易於理解、記憶、調試和修改
API-完整性原則
- 一個完整的API意味着實現它預期的功能。這和最小化的特性可能會產生沖突。另外如果一個成員函數出現在一個錯誤的類里,API的使用者們可能會找不到它
API-有明確和簡單的語意
- 正如其他的設計工作一樣,你應當遵守“最小驚奇”原則。一般情況下,這是容易做到的。請不要用解決方案所解決的問題過於籠統。(例如Qt3中的QMimeSourceFactory,應該被叫做 QImageLoader從而作為一個不同的API)
API-直觀性原則
- 不同的經歷和背景讓人們對什么具有“直觀性”什么不具有,有着不同的感覺。以下情況我們可以認為一個API是直觀的
- 一個稍有一些經驗的用戶在不看幫助文檔的情況下,能夠正確地使用該API
- 一個從不知道該API的用戶能夠看懂使用該API寫成的代碼
API-便於記住
- 選擇一個一致的和准確的命名約定
- 使用可識別的模式和概念
- 避免使用縮寫
API-易讀性原則
- 代碼是一次寫成,但需要多次閱讀,易讀的代碼可能寫的時候會花費稍長的時間,但是在整個產品的生命周期之中,但節省你很多閱讀和理解的時間
- 最后,記住不同的用戶會用到一個API的不同部分。雖然直接使用Qt的類生成一個實例是直觀的,但我們還是有理由期待用戶在派生一個Qt的類之前先閱讀它的官方文檔
API-靜態多態性
- 相似的類應該有相似的API,這可以用繼承的方式實現,這用到了運行時多態。
- 但是多態也可以發生在設計類的時候,例如,你把一個對象的類型從QProgressBar 換成Qslider,或者從QString 換成QByteArray,你會發現它們之間的API是何其的相似,以至於你可以很簡單地用一個去替換另一個。這就是我們所說的“靜態多態性”
- “靜態多態性”使得記住這些API和使用編程模式都更為簡單了。因此,為一系列相關類設計相似的API好過為每個類設計獨立的、更切合自身的API
- 一般來說,在Qt中我們偏愛使用“靜態多態性”而非實際的繼承,除非一些不可控制的原因要求我們不得不如此。
常引用
- 如果一個類型超過16個字節,用常引用傳遞它。
- 如果一個類型有一個非平凡拷貝構造函數(non-trivial copy-constructor),或一個非平凡析構函數(non-trivial destructor),用常引用傳遞它的值而避免使用以上方法
- 所有的其他類型都應該直接傳遞其值
void setAge(int age); void setCategory(QChar cat); void setName(QLatin1String name); void setAlarm(const QSharedPointer<Alarm> &alarm); // const-ref is much faster than running copy-constructor and destructor // QDate, QTime, QPoint, QPointF, QSize, QSizeF, QRect are good examples of other classes you should pass by value.
枚舉類型和枚舉值的命名
- 以下的示例說明了枚舉值命名時給出一般的名稱的危險
namespace Qt
{
enum Corner { TopLeft, BottomRight, … }; enum CaseSensitivity { Insensitive, Sensitive }; … };
tabWidget->setCornerWidget(widget, Qt::TopLeft); str.indexOf("$(QTDIR)", Qt::Insensitive);
- 在最后一行,Insensitive 是什么含義呢?這是不易於理解的。因此,枚舉值命名時,至少重復枚舉類型名中的一個字母
namespace Qt
{
enum Corner { TopLeftCorner, BottomRightCorner, … }; enum CaseSensitivity { CaseInsensitive, CaseSensitive }; … };
tabWidget->setCornerWidget(widget, Qt::TopLeftCorner); str.indexOf("$(QTDIR)", Qt::CaseInsensitive);
企圖少寫代碼的陷阱
- 不要為了圖方便少些一些代碼。因為代碼是一次書寫,后期不止一次地要去理解。例如
QSlider *slider = new QSlider(12, 18, 3, 13, Qt::Vertical, 0, "volume");
- 改成下面的方式會更容易理解
QSlider *slider = new QSlider(Qt::Vertical); slider->setRange(12, 18); slider->setPageStep(3); slider->setValue(13); slider->setObjectName("volume");
Bool型參數陷阱
- 這方面經典的例子是Qt中的repaint()函數,它的bool型參數用來標記窗口的背景是否被擦除。用法如下:
widget->repaint(false);
- 上面的代碼很容易被理解成“不重新繪制”
- 上面代碼的中用到的repaint()函數,其設計時的考慮無非是為了可以少定義一個函數,結果反而帶來了誤解,有多少人可以對以下三行代碼所代表隊含義進行准確地區分呢?
widget->repaint(); widget->repaint(true); widget->repaint(false);
- 稍微好一點的做法是這樣
widget->repaint(); widget->repaintWithoutErasing();
- 還有一個很明顯的做法是盡可能地用枚舉類型代替bool型參數,請對比以下的兩行代碼
str.replace("USER", user, false); // Qt 3 str.replace("USER", user, Qt::CaseInsensitive); // Qt 4
