Google C++ Style

http://www.cnblogs.com/kaiyang/archive/2008/10/07/1305475.html

Google的開源項目大多使用C++開發。每一個C++程序員也都知道,C++具有很多強大的語言特性,但這種強大不可避免的導致它的複雜,這種複雜會使得代碼更易於出現bug、難於閱讀和維護。

本指南的目的是通過詳細闡述在C++編碼時要怎樣寫、不要怎樣寫來規避其複雜性。這些規則可在允許代碼有效使用C++語言特性的同時使其易於管理。

風格,也被視爲可讀性,主要指稱管理C++代碼的習慣。使用術語風格有點用詞不當,因爲這些習慣遠不止源代碼文件格式這麼簡單。

使代碼易於管理的方法之一是增強代碼一致性,讓別人可以讀懂你的代碼是很重要的,保持統一編程風格意味着可以輕鬆根據模式匹配規則推斷各種符號的含義。創建通用的、必需的習慣用語和模式可以使代碼更加容易理解,在某些情況下改變一些編程風格可能會是好的選擇,但我們還是應該遵循一致性原則,儘量不這樣去做。

本指南的另一個觀點是C++特性的臃腫。C++是一門包含大量高級特性的巨型語言,某些情況下,我們會限制甚至禁止使用某些特性使代碼簡化,避免可能導致的各種問題,指南中列舉了這類特性,並解釋說爲什麼這些特性是被限制使用的。

Google開發的開源項目將遵照本指南約定。

注意:本指南並非C++教程,我們假定讀者已經對C++非常熟悉。

·         頭文件

通常,每一個.cc文件(C++的源文件)都有一個對應的.h文件(頭文件),也有一些例外,如單元測試代碼和只包含main().cc文件。

正確使用頭文件可令代碼在可讀性、文件大小和性能上大爲改觀。

下面的規則將引導你規避使用頭文件時的各種麻煩。

1. #define的保護

所有頭文件都應該使用#define防止頭文件被多重包含(multiple inclusion,命名格式當是:<PROJECT>_<PATH>_<FILE>_H_

爲保證唯一性,頭文件的命名應基於其所在項目源代碼樹的全路徑。例如,項目foo中的頭文件foo/src/bar/baz.h按如下方式保護:

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_

2. 頭文件依賴

使用前置聲明(forward declarations儘量減少.h文件中#include的數量。

當一個頭文件被包含的同時也引入了一項新的依賴(dependency,只要該頭文件被修改,代碼就要重新編譯。如果你的頭文件包含了其他頭文件,這些頭文件的任何改變也將導致那些包含了你的頭文件的代碼重新編譯。因此,我們寧可儘量少包含頭文件,尤其是那些包含在其他頭文件中的。

使用前置聲明可以顯著減少需要包含的頭文件數量。舉例說明:頭文件中用到類File,但不需要訪問File的聲明,則頭文件中只需前置聲明class File;無需#include "file/base/file.h"

在頭文件如何做到使用類Foo而無需訪問類的定義?

1) 將數據成員類型聲明爲Foo *Foo &

2) 參數、返回值類型爲Foo的函數只是聲明(但不定義實現);

3) 靜態數據成員的類型可以被聲明爲Foo,因爲靜態數據成員的定義在類定義之外。

另一方面,如果你的類是Foo的子類,或者含有類型爲Foo的非靜態數據成員,則必須爲之包含頭文件。

有時,使用指針成員(pointer members,如果是scoped_ptr更好)替代對象成員(object members)的確更有意義。然而,這樣的做法會降低代碼可讀性及執行效率。如果僅僅爲了少包含頭文件,還是不要這樣替代的好。

當然,.cc文件無論如何都需要所使用類的定義部分,自然也就會包含若干頭文件。

譯者注:能依賴聲明的就不要依賴定義。

3. 內聯函數

只有當函數只有10行甚至更少時纔會將其定義爲內聯函數(inline function

定義(Definition):當函數被聲明爲內聯函數之後,編譯器可能會將其內聯展開,無需按通常的函數調用機制調用內聯函數。

優點:當函數體比較小的時候,內聯該函數可以令目標代碼更加高效。對於存取函數(accessormutator以及其他一些比較短的關鍵執行函數。

缺點:濫用內聯將導致程序變慢,內聯有可能是目標代碼量或增或減,這取決於被內聯的函數的大小。內聯較短小的存取函數通常會減少代碼量,但內聯一個很大的函數(譯者注:如果編譯器允許的話)將戲劇性的增加代碼量。在現代處理器上,由於更好的利用指令緩存(instruction cache,小巧的代碼往往執行更快。

結論:一個比較得當的處理規則是,不要內聯超過10行的函數。對於析構函數應慎重對待,析構函數往往比其表面看起來要長,因爲有一些隱式成員和基類析構函數(如果有的話)被調用!

另一有用的處理規則:內聯那些包含循環或switch語句的函數是得不償失的,除非在大多數情況下,這些循環或switch語句從不執行。

重要的是,虛函數和遞歸函數即使被聲明爲內聯的也不一定就是內聯函數。通常,遞歸函數不應該被聲明爲內聯的(譯者注:遞歸調用堆棧的展開並不像循環那麼簡單,比如遞歸層數在編譯時可能是未知的,大多數編譯器都不支持內聯遞歸函數)。析構函數內聯的主要原因是其定義在類的定義中,爲了方便抑或是對其行爲給出文檔。

4. -inl.h文件

複雜的內聯函數的定義,應放在後綴名爲-inl.h的頭文件中。

在頭文件中給出內聯函數的定義,可令編譯器將其在調用處內聯展開。然而,實現代碼應完全放到.cc文件中,我們不希望.h文件中出現太多實現代碼,除非這樣做在可讀性和效率上有明顯優勢。

如果內聯函數的定義比較短小、邏輯比較簡單,其實現代碼可以放在.h文件中。例如,存取函數的實現理所當然都放在類定義中。出於實現和調用的方便,較複雜的內聯函數也可以放到.h文件中,如果你覺得這樣會使頭文件顯得笨重,還可以將其分離到單獨的-inl.h中。這樣即把實現和類定義分離開來,當需要時包含實現所在的-inl.h即可。

-inl.h文件還可用於函數模板的定義,從而使得模板定義可讀性增強。

要提醒的一點是,-inl.h和其他頭文件一樣,也需要#define保護。

5. 函數參數順序(Function Parameter Ordering

定義函數時,參數順序爲:輸入參數在前,輸出參數在後。

C/C++函數參數分爲輸入參數和輸出參數兩種,有時輸入參數也會輸出(譯者注:值被修改時)。輸入參數一般傳值或常數引用(const references,輸出參數或輸入/輸出參數爲非常數指針(non-const pointers。對參數排序時,將所有輸入參數置於輸出參數之前。不要僅僅因爲是新添加的參數,就將其置於最後,而應該依然置於輸出參數之前。

這一點並不是必須遵循的規則,輸入/輸出兩用參數(通常是類/結構體變量)混在其中,會使得規則難以遵循。

6. 包含文件的名稱及次序

將包含次序標準化可增強可讀性、避免隱藏依賴(hidden dependencies,譯者注:隱藏依賴主要是指包含的文件中編譯時),次序如下:C庫、C++庫、其他庫的.h、項目內的.h

項目內頭文件應按照項目源代碼目錄樹結構排列,並且避免使用UNIX文件路徑.(當前目錄)和..(父目錄)。例如,google-awesome-project/src/base/logging.h應像這樣被包含:

#include "base/logging.h"

dir/foo.cc的主要作用是執行或測試dir2/foo2.h的功能,foo.cc中包含頭文件的次序如下:

    dir2/foo2.h(優先位置,詳情如下)
    C
系統文件
    C++
系統文件
   
其他庫頭文件
   
本項目內頭文件

這種排序方式可有效減少隱藏依賴,我們希望每一個頭文件獨立編譯。最簡單的實現方式是將其作爲第一個.h文件包含在對應的.cc中。

dir/foo.ccdir2/foo2.h通常位於相同目錄下(像base/basictypes_unittest.ccbase/basictypes.h),但也可在不同目錄下。

相同目錄下頭文件按字母序是不錯的選擇。

舉例來說,google-awesome-project/src/foo/internal/fooserver.cc的包含次序如下:

#include "foo/public/fooserver.h"  // 優先位置

#include <sys/types.h>
#include <unistd.h>

#include <hash_map>
#include <vector>

#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"

______________________________________

譯者:英語不太好,翻譯的也就不太好。這一篇主要提到的是頭文件的一些規則,總結一下:

1. 避免多重包含是學編程時最基本的要求;

2. 前置聲明是爲了降低編譯依賴,防止修改一個頭文件引發多米諾效應;

3. 內聯函數的合理使用可提高代碼執行效率;

4. -inl.h可提高代碼可讀性(一般用不到吧:D);

5. 標準化函數參數順序可以提高可讀性和易維護性(對函數參數的堆棧空間有輕微影響,我以前大多是相同類型放在一起);

6. 包含文件的名稱使用...雖然方便卻易混亂,使用比較完整的項目路徑看上去很清晰、很條理,包含文件的次序除了美觀之外,最重要的是可以減少隱藏依賴,使每個頭文件在最需要編譯(對應源文件處:D)的地方編譯,有人提出庫文件放在最後,這樣出錯先是項目內的文件,頭文件都放在對應源文件的最前面,這一點足以保證內部錯誤的及時發現了。

 

 

·         作用域

1. 命名空間(Namespaces

.cc文件中,提倡使用不具名的命名空間(unnamed namespaces,譯者注:不具名的命名空間就像不具名的類一樣,似乎被介紹的很少:-(。使用具名命名空間時,其名稱可基於項目或路徑名稱,不要使用using指示符。

定義:命名空間將全局作用域細分爲不同的、具名的作用域,可有效防止全局作用域的命名衝突。

優點:命名空間提供了(可嵌套)命名軸線(name axis,譯者注:將命名分割在不同命名空間內),當然,類也提供了(可嵌套)的命名軸線(譯者注:將命名分割在不同類的作用域內)。

舉例來說,兩個不同項目的全局作用域都有一個類Foo,這樣在編譯或運行時造成衝突。如果每個項目將代碼置於不同命名空間中,project1::Fooproject2::Foo作爲不同符號自然不會衝突。

缺點:命名空間具有迷惑性,因爲它們和類一樣提供了額外的(可嵌套的)命名軸線。在頭文件中使用不具名的空間容易違背C++唯一定義原則(One Definition Rule (ODR)

結論:根據下文將要提到的策略合理使用命名空間。

1) 不具名命名空間(Unnamed Namespaces

.cc文件中,允許甚至提倡使用不具名命名空間,以避免運行時的命名衝突:

namespace {                                   // .cc 文件中

// 命名空間的內容無需縮進
enum { UNUSED, EOF, ERROR };          //
經常使用的符號
bool AtEof() { return pos_ == EOF; }   //
使用本命名空間內的符號EOF

}  // namespace

然而,與特定類關聯的文件作用域聲明在該類中被聲明爲類型、靜態數據成員或靜態成員函數,而不是不具名命名空間的成員。像上文展示的那樣,不具名命名空間結束時用註釋// namespace標識。

不能在.h文件中使用不具名命名空間。

2) 具名命名空間(Named Namespaces

具名命名空間使用方式如下:

命名空間將除文件包含、全局標識的聲明/定義以及類的前置聲明外的整個源文件封裝起來,以同其他命名空間相區分。

// .h文件
namespace mynamespace {

// 所有聲明都置於命名空間中
//
注意不要使用縮進
class MyClass {
public:
  ...
  void Foo();
};

}  // namespace mynamespace

// .cc文件
namespace mynamespace {

// 函數定義都置於命名空間中
void MyClass::Foo() {
  ...
}

}  // namespace mynamespace

通常的.cc文件會包含更多、更復雜的細節,包括對其他命名空間中類的引用等。

#include "a.h"

DEFINE_bool(someflag, false, "dummy flag");

class C;  // 全局命名空間中類C的前置聲明
namespace a { class A; }  //
命名空間a中的類a::A的前置聲明

namespace b {

...code for b...                // b中的代碼

}  // namespace b

不要聲明命名空間std下的任何內容,包括標準庫類的前置聲明。聲明std下的實體會導致不明確的行爲,如,不可移植。聲明標準庫下的實體,需要包含對應的頭文件。

最好不要使用using指示符,以保證命名空間下的所有名稱都可以正常使用。

// 禁止——污染命名空間
using namespace foo;

.cc文件、.h文件的函數、方法或類中,可以使用using

// 允許:.cc文件中
// .h
文件中,必須在函數、方法或類的內部使用
using ::foo::bar;

.cc文件、.h文件的函數、方法或類中,還可以使用命名空間別名。

// 允許:.cc文件中
// .h
文件中,必須在函數、方法或類的內部使用

namespace fbz = ::foo::bar::baz;

2. 嵌套類(Nested Class

當公開嵌套類作爲接口的一部分時,雖然可以直接將他們保持在全局作用域中,但將嵌套類的聲明置於命名空間中是更好的選擇。

定義:可以在一個類中定義另一個類,嵌套類也稱成員類(member class

class Foo {

private:
  // Bar
是嵌套在Foo中的成員類
  class Bar {
    ...
  };

};

優點:當嵌套(成員)類只在被嵌套類(enclosing class中使用時很有用,將其置於被嵌套類作用域作爲被嵌套類的成員不會污染其他作用域同名類。可在被嵌套類中前置聲明嵌套類,在.cc文件中定義嵌套類,避免在被嵌套類中包含嵌套類的定義,因爲嵌套類的定義通常只與實現相關。

缺點:只能在被嵌套類的定義中才能前置聲明嵌套類。因此,任何使用Foo::Bar*指針的頭文件必須包含整個Foo的聲明。

結論:不要將嵌套類定義爲public,除非它們是接口的一部分,比如,某個方法使用了這個類的一系列選項。

3. 非成員函數(Nonmember)、靜態成員函數(Static Member)和全局函數(Global Functions

使用命名空間中的非成員函數或靜態成員函數,儘量不要使用全局函數。

優點:某些情況下,非成員函數和靜態成員函數是非常有用的,將非成員函數置於命名空間中可避免對全局作用域的污染。

缺點:將非成員函數和靜態成員函數作爲新類的成員或許更有意義,當它們需要訪問外部資源或具有重要依賴時更是如此。

結論:

有時,不把函數限定在類的實體中是有益的,甚至需要這麼做,要麼作爲靜態成員,要麼作爲非成員函數。非成員函數不應依賴於外部變量,並儘量置於某個命名空間中。相比單純爲了封裝若干不共享任何靜態數據的靜態成員函數而創建類,不如使用命名空間。

定義於同一編譯單元的函數,被其他編譯單元直接調用可能會引入不必要的耦合和連接依賴;靜態成員函數對此尤其敏感。可以考慮提取到新類中,或者將函數置於獨立庫的命名空間中。

如果你確實需要定義非成員函數,又只是在.cc文件中使用它,可使用不具名命名空間或static關聯(如static int Foo() {...})限定其作用域。

4. 局部變量(Local Variables

將函數變量儘可能置於最小作用域內,在聲明變量時將其初始化。

C++允許在函數的任何位置聲明變量。我們提倡在儘可能小的作用域中聲明變量,離第一次使用越近越好。這使得代碼易於閱讀,易於定位變量的聲明位置、變量類型和初始值。特別是,應使用初始化代替聲明+賦值的方式。

int i;
i = f();        //
——初始化和聲明分離
int j = g();   //
——初始化時聲明

注意:gcc可正確執行for (int i = 0; i < 10; ++i)i的作用域僅限for循環),因此其他for循環中可重用iifwhile等語句中,作用域聲明(scope declaration同樣是正確的。

while (const char* p = strchr(str, '/')) str = p + 1;

注意:如果變量是一個對象,每次進入作用域都要調用其構造函數,每次退出作用域都要調用其析構函數。

// 低效的實現
for (int i = 0; i < 1000000; ++i) {
  Foo f;  //
構造函數和析構函數分別調用1000000次!
  f.DoSomething(i);
}

類似變量放到循環作用域外面聲明要高效的多:

Foo f;  // 構造函數和析構函數只調用1
for (int i = 0; i < 1000000; ++i) {
  f.DoSomething(i);
}

5. 全局變量(Global Variables

class類型的全局變量是被禁止的,內建類型的全局變量是允許的,當然多線程代碼中非常數全局變量也是被禁止的。永遠不要使用函數返回值初始化全局變量。

不幸的是,全局變量的構造函數、析構函數以及初始化操作的調用順序只是被部分規定,每次生成有可能會有變化,從而導致難以發現的bugs

因此,禁止使用class類型的全局變量(包括STLstring, vector等等),因爲它們的初始化順序有可能導致構造出現問題。內建類型和由內建類型構成的沒有構造函數的結構體可以使用,如果你一定要使用class類型的全局變量,請使用單件模式(singleton pattern

對於全局的字符串常量,使用C風格的字符串,而不要使用STL的字符串:

const char kFrogSays[] = "ribbet";

雖然允許在全局作用域中使用全局變量,使用時務必三思。大多數全局變量應該是類的靜態數據成員,或者當其只在.cc文件中使用時,將其定義到不具名命名空間中,或者使用靜態關聯以限制變量的作用域。

記住,靜態成員變量視作全局變量,所以,也不能是class類型!

______________________________________

譯者:這一篇主要提到的是作用域的一些規則,總結一下:

1. .cc中的不具名命名空間可避免命名衝突、限定作用域,避免直接使用using提示符污染命名空間;

2. 嵌套類符合局部使用原則,只是不能在其他頭文件中前置聲明,儘量不要public

3. 儘量不用全局函數和全局變量,考慮作用域和命名空間限制,儘量單獨形成編譯單元;

4. 多線程中的全局變量(含靜態成員變量)不要使用class類型(含STL容器),避免不明確行爲導致的bugs

作用域的使用,除了考慮名稱污染、可讀性之外,主要是爲降低耦合度,提高編譯、執行效率。

這一篇主要提到的是類,Lippman在《Inside The C++ Object Model》第二章中對構造函數作了詳盡說明,本文中提到的幾個單詞基本仿該書中譯本侯捷先生的翻譯:

explicit:明確的

implicit:隱含的

trivial:沒有意義的

non-trivial:有意義的

 

原文地址:Google C++ Style Guide

·        

類是C++中基本的代碼單元,自然被廣泛使用。本節列舉了在寫一個類時要做什麼、不要做什麼。

1. 構造函數(Constructor)的職責

構造函數中只進行那些沒有實際意義的(trivial,譯者注:簡單初始化對於程序執行沒有實際的邏輯意義,因爲成員變量的有意義的值大多不在構造函數中確定)初始化,可能的話,使用Init()方法集中初始化爲有意義的(non-trivial數據。

定義:在構造函數中執行初始化操作。

優點:排版方便,無需擔心類是否初始化。

缺點:在構造函數中執行操作引起的問題有:

1) 構造函數中不易報告錯誤,不能使用異常。

2) 操作失敗會造成對象初始化失敗,引起不確定狀態。

3) 構造函數內調用虛函數,調用不會派發到子類實現中,即使當前沒有子類化實現,將來仍是隱患。

4) 如果有人創建該類型的全局變量(雖然違背了上節提到的規則),構造函數將在main()之前被調用,有可能破壞構造函數中暗含的假設條件。例如,gflags尚未初始化。

結論:如果對象需要有意義的(non-trivial初始化,考慮使用另外的Init()方法並(或)增加一個成員標記用於指示對象是否已經初始化成功。

2. 默認構造函數(Default Constructors

如果一個類定義了若干成員變量又沒有其他構造函數,需要定義一個默認構造函數,否則編譯器將自動生產默認構造函數。

定義:新建一個沒有參數的對象時,默認構造函數被調用,當調用new[](爲數組)時,默認構造函數總是被調用。

優點:默認將結構體初始化爲不可能的值,使調試更加容易。

缺點:對代碼編寫者來說,這是多餘的工作。

結論:

如果類中定義了成員變量,沒有提供其他構造函數,你需要定義一個默認構造函數(沒有參數)。默認構造函數更適合於初始化對象,使對象內部狀態(internal state一致、有效。

提供默認構造函數的原因是:如果你沒有提供其他構造函數,又沒有定義默認構造函數,編譯器將爲你自動生成一個,編譯器生成的構造函數並不會對對象進行初始化。

如果你定義的類繼承現有類,而你又沒有增加新的成員變量,則不需要爲新類定義默認構造函數。

3. 明確的構造函數(Explicit Constructors

對單參數構造函數使用C++關鍵字explicit

定義:通常,只有一個參數的構造函數可被用於轉換(conversion,譯者注:主要指隱式轉換,下文可見),例如,定義了Foo::Foo(string name),當向需要傳入一個Foo對象的函數傳入一個字符串時,構造函數Foo::Foo(string name)被調用並將該字符串轉換爲一個Foo臨時對象傳給調用函數。看上去很方便,但如果你並不希望如此通過轉換生成一個新對象的話,麻煩也隨之而來。爲避免構造函數被調用造成隱式轉換,可以將其聲明爲explicit

優點:避免不合時宜的變換。

缺點:無。

結論:

所有單參數構造函數必須是明確的。在類定義中,將關鍵字explicit加到單參數構造函數前:explicit Foo(string name);

例外:在少數情況下,拷貝構造函數可以不聲明爲explicit;特意作爲其他類的透明包裝器的類。類似例外情況應在註釋中明確說明。

4. 拷貝構造函數(Copy Constructors

僅在代碼中需要拷貝一個類對象的時候使用拷貝構造函數;不需要拷貝時應使用DISALLOW_COPY_AND_ASSIGN

定義:通過拷貝新建對象時可使用拷貝構造函數(特別是對象的傳值時)。

優點:拷貝構造函數使得拷貝對象更加容易,STL容器要求所有內容可拷貝、可賦值。

缺點:C++中對象的隱式拷貝是導致很多性能問題和bugs的根源。拷貝構造函數降低了代碼可讀性,相比按引用傳遞,跟蹤按值傳遞的對象更加困難,對象修改的地方變得難以捉摸。

結論:

大量的類並不需要可拷貝,也不需要一個拷貝構造函數或賦值操作(assignment operator。不幸的是,如果你不主動聲明它們,編譯器會爲你自動生成,而且是public的。

可以考慮在類的private中添加空的(dummy拷貝構造函數和賦值操作,只有聲明,沒有定義。由於這些空程序聲明爲private,當其他代碼試圖使用它們的時候,編譯器將報錯。爲了方便,可以使用宏DISALLOW_COPY_AND_ASSIGN

// 禁止使用拷貝構造函數和賦值操作的宏
// 應在類的private:中使用
#define DISALLOW_COPY_AND_ASSIGN(TypeName) "
  TypeName(const TypeName&);               "
  void operator=(const TypeName&)

class Foo {
public:
  Foo(int f);
  ~Foo();

private:
  DISALLOW_COPY_AND_ASSIGN(Foo);
};

如上所述,絕大多數情況下都應使用DISALLOW_COPY_AND_ASSIGN,如果類確實需要可拷貝,應在該類的頭文件中說明原由,並適當定義拷貝構造函數和賦值操作,注意在operator=中檢測自賦值(self-assignment情況。

在將類作爲STL容器值得時候,你可能有使類可拷貝的衝動。類似情況下,真正該做的是使用指針指向STL容器中的對象,可以考慮使用std::tr1::shared_ptr

5. 結構體和類(Structs vs. Classes

僅當只有數據時使用struct,其它一概使用class

C++中,關鍵字structclass幾乎含義等同,我們爲其人爲添加語義,以便爲定義的數據類型合理選擇使用哪個關鍵字。

struct被用在僅包含數據的消極對象(passive objects)上,可能包括有關聯的常量,但沒有存取數據成員之外的函數功能,而存取功能通過直接訪問實現而無需方法調用,這兒提到的方法是指只用於處理數據成員的,如構造函數、析構函數、Initialize()Reset()Validate()

如果需要更多的函數功能,class更適合,如果不確定的話,直接使用class

如果與STL結合,對於仿函數(functors)和特性(traits可以不用class而是使用struct

注意:類和結構體的成員變量使用不同的命名規則。

6. 繼承(Inheritance

使用組合(composition,譯者注,這一點也是GoF在《Design Patterns》裏反覆強調的)通常比使用繼承更適宜,如果使用繼承的話,只使用公共繼承。

定義:當子類繼承基類時,子類包含了父基類所有數據及操作的定義。C++實踐中,繼承主要用於兩種場合:實現繼承(implementation inheritance,子類繼承父類的實現代碼;接口繼承(interface inheritance,子類僅繼承父類的方法名稱。

優點:實現繼承通過原封不動的重用基類代碼減少了代碼量。由於繼承是編譯時聲明(compile-time declaration,編碼者和編譯器都可以理解相應操作並發現錯誤。接口繼承可用於程序上增強類的特定API的功能,在類沒有定義API的必要實現時,編譯器同樣可以偵錯。

缺點:對於實現繼承,由於實現子類的代碼在父類和子類間延展,要理解其實現變得更加困難。子類不能重寫父類的非虛函數,當然也就不能修改其實現。基類也可能定義了一些數據成員,還要區分基類的物理輪廓(physical layout

結論:

所有繼承必須是public的,如果想私有繼承的話,應該採取包含基類實例作爲成員的方式作爲替代。

不要過多使用實現繼承,組合通常更合適一些。努力做到只在是一個"is-a",譯者注,其他"has-a"情況下請使用組合)的情況下使用繼承:如果Bar的確是一種”Foo,才令BarFoo的子類。

必要的話,令析構函數爲virtual,必要是指,如果該類具有虛函數,其析構函數應該爲虛函數。

譯者注:至於子類沒有額外數據成員,甚至父類也沒有任何數據成員的特殊情況下,析構函數的調用是否必要是語義爭論,從編程設計規範的角度看,在含有虛函數的父類中,定義虛析構函數絕對必要。

限定僅在子類訪問的成員函數爲protected,需要注意的是數據成員應始終爲私有。

當重定義派生的虛函數時,在派生類中明確聲明其爲virtual。根本原因:如果遺漏virtual,閱讀者需要檢索類的所有祖先以確定該函數是否爲虛函數(譯者注,雖然不影響其爲虛函數的本質)

7. 多重繼承(Multiple Inheritance

真正需要用到多重實現繼承(multiple implementation inheritance的時候非常少,只有當最多一個基類中含有實現,其他基類都是以Interface爲後綴的純接口類時纔會使用多重繼承。

定義:多重繼承允許子類擁有多個基類,要將作爲純接口的基類和具有實現的基類區別開來。

優點:相比單繼承,多重實現繼承可令你重用更多代碼。

缺點:真正需要用到多重實現繼承的時候非常少,多重實現繼承看上去是不錯的解決方案,通常可以找到更加明確、清晰的、不同的解決方案。

結論:只有當所有超類(superclass除第一個外都是純接口時才能使用多重繼承。爲確保它們是純接口,這些類必須以Interface爲後綴。

注意:關於此規則,Windows下有種例外情況(譯者注,將在本譯文最後一篇的規則例外中闡述)。

8. 接口(Interface

接口是指滿足特定條件的類,這些類以Interface爲後綴(非必需)。

定義:當一個類滿足以下要求時,稱之爲純接口:

1) 只有純虛函數("=0")和靜態函數(下文提到的析構函數除外);

2) 沒有非靜態數據成員;

3) 沒有定義任何構造函數。如果有,也不含參數,並且爲protected

4) 如果是子類,也只能繼承滿足上述條件並以Interface爲後綴的類。

接口類不能被直接實例化,因爲它聲明瞭純虛函數。爲確保接口類的所有實現可被正確銷燬,必須爲之聲明虛析構函數(作爲第1條規則的例外,析構函數不能是純虛函數)。具體細節可參考Stroustrup的《The C++ Programming Language, 3rd edition》第12.4節。

優點:Interface爲後綴可令他人知道不能爲該接口類增加實現函數或非靜態數據成員,這一點對於多重繼承尤其重要。另外,對於Java程序員來說,接口的概念已經深入人心。

缺點:Interface後綴增加了類名長度,爲閱讀和理解帶來不便,同時,接口特性作爲實現細節不應暴露給客戶。

結論:。只有在滿足上述需要時,類才以Interface結尾,但反過來,滿足上述需要的類未必一定以Interface結尾。

9. 操作符重載(Operator Overloading

除少數特定環境外,不要重載操作符。

定義:一個類可以定義諸如+/等操作符,使其可以像內建類型一樣直接使用。

優點:使代碼看上去更加直觀,就像內建類型(如int)那樣,重載操作符使那些Equals()Add()等黯淡無光的函數名好玩多了。爲了使一些模板函數正確工作,你可能需要定義操作符。

缺點:雖然操作符重載令代碼更加直觀,但也有一些不足

1) 混淆直覺,讓你誤以爲一些耗時的操作像內建操作那樣輕巧;

2) 查找重載操作符的調用處更加困難,查找Equals()顯然比同等調用==容易的多;

3) 有的操作符可以對指針進行操作,容易導致bugsFoo + 4做的是一件事,而&Foo + 4可能做的是完全不同的另一件事,對於二者,編譯器都不會報錯,使其很難調試;

4) 重載還有令你吃驚的副作用,比如,重載操作符&的類不能被前置聲明。

結論:

一般不要重載操作符,尤其是賦值操作(operator=)比較陰險,應避免重載。如果需要的話,可以定義類似Equals()CopyFrom()等函數。

然而,極少數情況下需要重載操作符以便與模板或標準”C++類銜接(如operator<<(ostream&, const T&)),如果被證明是正當的尚可接受,但你要儘可能避免這樣做。尤其是不要僅僅爲了在STL容器中作爲key使用就重載operator==operator<,取而代之,你應該在聲明容器的時候,創建相等判斷和大小比較的仿函數類型。

有些STL算法確實需要重載operator==時可以這麼做,不要忘了提供文檔說明原因。

參考拷貝構造函數函數重載

10. 存取控制(Access Control

將數據成員私有化,並提供相關存取函數,如定義變量foo_及取值函數foo()、賦值函數set_foo()

存取函數的定義一般內聯在頭文件中。

參考繼承函數命名

11. 聲明次序(Declaration Order

在類中使用特定的聲明次序:public:private:之前,成員函數在數據成員(變量)前。

定義次序如下:public:protected:private:,如果那一塊沒有,直接忽略即可。

每一塊中,聲明次序一般如下:

1) typedefsenums

2) 常量;

3) 構造函數;

4) 析構函數;

5) 成員函數,含靜態成員函數;

6) 數據成員,含靜態數據成員。

DISALLOW_COPY_AND_ASSIGN置於private:塊之後,作爲類的最後部分。參考拷貝構造函數

.cc文件中函數的定義應儘可能和聲明次序一致。

不要將大型函數內聯到類的定義中,通常,只有那些沒有特別意義的或者性能要求高的,並且是比較短小的函數才被定義爲內聯函數。更多細節參考譯文第一篇的內聯函數

12. 編寫短小函數(Write Short Functions

傾向於選擇短小、凝練的函數。

長函數有時是恰當的,因此對於函數長度並沒有嚴格限制。如果函數超過40行,可以考慮在不影響程序結構的情況下將其分割一下。

即使一個長函數現在工作的非常好,一旦有人對其修改,有可能出現新的問題,甚至導致難以發現的bugs。使函數儘量短小、簡單,便於他人閱讀和修改代碼。

在處理代碼時,你可能會發現複雜的長函數,不要害怕修改現有代碼:如果證實這些代碼使用、調試困難,或者你需要使用其中的一小塊,考慮將其分割爲更加短小、易於管理的若干函數。

______________________________________

譯者:關於類的注意事項,總結一下:

1. 不在構造函數中做太多邏輯相關的初始化;

2. 編譯器提供的默認構造函數不會對變量進行初始化,如果定義了其他構造函數,編譯器不再提供,需要編碼者自行提供默認構造函數;

3. 爲避免隱式轉換,需將單參數構造函數聲明爲explicit

4. 爲避免拷貝構造函數、賦值操作的濫用和編譯器自動生成,可目前聲明其爲private且無需實現;

5. 僅在作爲數據集合時使用struct

6. 組合>實現繼承>接口繼承>私有繼承,子類重載的虛函數也要聲明virtual關鍵字,雖然編譯器允許不這樣做;

7. 避免使用多重繼承,使用時,除一個基類含有實現外,其他基類均爲純接口;

8. 接口類類名以Interface爲後綴,除提供帶實現的虛析構函數、靜態成員函數外,其他均爲純虛函數,不定義非靜態數據成員,不提供構造函數,提供的話,聲明爲protected

9. 爲降低複雜性,儘量不重載操作符,模板、標準類中使用時提供文檔說明;

10. 存取函數一般內聯在頭文件中;

11. 聲明次序:public->protected->private

12. 函數體儘量短小、緊湊,功能單一。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章