Google C++程式設計風格指南(三):C++ 類

yangdelong發表於2009-10-10

http://www.kuqin.com/language/20080717/11552.html

 

關於類的注意事項,總結一下:1. 不在建構函式中做太多邏輯相關的初始化; 2. 編譯器提供的預設建構函式不會對變數進行初始化,如果定義了其他建構函式,編譯器不再提供,需要編碼者自行提供預設建構函式;3. 為避免隱式轉換,需將單引數建構函式宣告為explicit;……

類是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++中,關鍵字struct和class幾乎含義等同,我們為其人為新增語義,以便為定義的資料型別合理選擇使用哪個關鍵字。

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,才令Bar是Foo的子類。

必要的話,令解構函式為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) 有的操作符可以對指標進行操作,容易導致bugs,Foo + 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) typedefs和enums;

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. 函式體儘量短小、緊湊,功能單一。

來自:http://www.cppblog.com/Fox/archive/2008/07/16/56324.html
原文:Google C++ Style Guide

 

相關文章