4 類
在C++中類是構成代碼的基礎。平時,我們非常廣泛的使用它們。這部分列出了當寫一個類時你應該遵循的重要的那些該做的和不該做的。
4.1 在構造函數中做事情
一般來說,構造函數應該僅僅是設置成員變量的初始值。任何複雜的初始化應該放在顯式的Init()方法裏。
Ø 定義
在構造函數中進行初始化是合理的。
Ø 正面的
便於打字。不用再擔心類是否被初始化了。
Ø 反面的
在構造函數中做事情的問題是:
l 沒有從構造函數中通知錯誤的簡單辦法,出了異常外(只是被禁止的)
l 如果構造函數中的工作失敗了,那麼我們就有了一個對象,而它的初始化代碼失敗了,那麼它可能出於未定義狀態。
l 如果這些事情調用了虛函數,這些調用將不會分配到子類的實現。你對類的進一步的改變可能悄悄引入這一問題,即使你的類現在還沒有子類,這會造成很大混亂。
l 如果有人用這個類型生成了一個全局的變量(這違反了規則,但是仍然這麼做了),構造函數將會在main()前被調用,這可能打破一些在構造函數代碼中隱藏的假定。比如gflags還沒有被初始化過。
Ø 結論
如果你的對象需要一些重大的初始化,考慮一下使用一個顯式的Init()方法。特別地,構造函數不應該調用虛函數,試圖報出錯誤,訪問可能沒有初始化的全局變量,等等。
4.2 默認構造函數
如果你的類定義了成員變量,並且沒有其他的構造函數,那麼你必須定義默認構造函數。否則編譯器會爲你生成一個—非常糟糕的。
Ø 定義
當我們new了一個對象,並且沒有參數,那麼默認構造函數將會被調用。在調用new[](數組)它也總會被調用。
Ø 正面的
使用默認值廚師化結構體,設置一些不可能的值,這樣會使debug更容易。
Ø 反面的
這會爲你(代碼機器)帶來一些額外的工作。
Ø 結論
如果你的類定義了成員變量,而且也沒有其它的構造函數,那麼你必須定義一個默認構造函數(默認構造函數是無參數的)。這個默認構造函數應該把對象初始化,使它的內部狀態一致,並有效。
這樣做的原因是,如果你沒有其它的構造函數,而且也沒有定義一個默認構造函數,那麼編譯器就會爲你生成一個。編譯器生成的構造函數對你的對象的初始化可沒那麼智能。
如果你的類從一個現有類繼承,而且你也沒有添加新的成員變量,那你就不用再定義默認構造函數了。
4.3 顯示構造函數
如果構造函數只有一個參數,給他加上C++關鍵字explicit。
Ø 定義
一般情況下,如果一個構造函數只接受一個參數,它是可能被用來做轉換的。比如,如果你定義了Foo::Foo(string name),然後你把一個string給了一個函數,而這個函數要的是Foo,這個構造函數就會被調用把string轉換爲Foo,併爲你把Foo傳給你的函數。有時候這是很方便的,但是這也是麻煩的製造者—轉換髮生了,而生成的新對象不是你想要的。把構造函數聲明爲explicit能阻止它被作爲轉換進行隱式調用。
Ø 正面的
避免不良的轉換。
Ø 反面的
無。
Ø 結論
我們要求所有的單參數構造函數都要是顯式的。在類的定義中,總是把explicit放在單參數構造函數前:explicit Foo(string name)。
拷貝構造函數是個例外,在少數情況下,我們允許它們不用explicit。還有一種例外就是,有些類就是爲了透明的封裝其它的類爲目的的。這些例外情況都應該以註釋註明。
4.4 拷貝構造函數
只在有必要的時候提供拷貝構造函數和賦值操作符。否則,用DISALLOW_COPY_AND_ASSIGN宏禁用它們。
Ø 定義
拷貝構造函數和賦值操作符是用來通過拷貝生成對象的。在某些情況下,拷貝構造函數被編譯器隱式調用,比如以傳值方式來傳遞對象。
Ø 正面的
拷貝構造函數是對象拷貝操作更容易。STL容器要求他的內容都支持拷貝和賦值。拷貝構造函數會比CopyFrom()的變通方式更有效率,因爲它把構造和拷貝融合在了一起,編譯器可根據上下文忽略它,而且它也使避免堆分配變得更簡單。
Ø 反面的
在C++中,隱式的對象拷貝是bug和性能問題的豐富源泉。同時,它也降低了可讀性,因爲與引用傳遞相比,很難跟蹤哪些對象被值傳遞,對一個對象的哪些改變其作用了。
Ø 結論
很少類需要支持拷貝。絕大多數既不需要拷貝構造,也不需要賦值操作符。在許多情況下,指針或引用可以工作得和值拷貝一樣好,而且具有更好的性能。例如,你可以對函數參數使用引用傳遞或用指針而不是值傳遞,而且你可以在STL容器中存儲指針而不是對象。
如果你的淚需要支持拷貝,最好提供專門的拷貝方法,比如像CopyFrom()
或 Clone()
,而不是拷貝構造函數,因爲這些方法不會被隱式調用。如果拷貝方法滿足不了你的需求(比如,由於性能原因,或者因爲你的類需要在STL容器中以值存儲),那麼就同時提供拷貝構造和賦值操作符。
如果你的類不需要拷貝構造函數或者賦值操作符,你必須顯式的禁掉它們。你應該這樣做,在你的類的private:區域聲明假的拷貝構造函數和賦值操作符,而不提供相應的定義(因此,所以試圖使用它們的都會造成鏈接錯誤)。
例如,可以用一個DISALLOW_COPY_AND_ASSIGN宏:
// 一個禁止拷貝構造函數和賦值操作符的宏
// 它應該放在類聲明的private:私有區
#define DISALLOW_COPY_AND_ASSIGN(TypeName) /
TypeName(const TypeName&); /
void operator=(const TypeName&)
在類Foo中:
class Foo {
public:
Foo(int f);
~Foo();
private:
DISALLOW_COPY_AND_ASSIGN(Foo);
};
4.5 結構體vs類
對那些用來存放數據的被動對象使用struct;其它的都用class。
在C++中struct和class關鍵字基本上是一樣的。我們給這兩個關鍵字加上我們的語義,所以你應該爲你定義的數據類型使用正確的關鍵字。
Structs用來表示那些存放數據的被動對象,而且可能有相關常數,但是除了用來訪問,設置數據字段外,沒有任何其它功能。對字段的訪問/設置是通過對字段的直接訪問完成的,而不是通過方法調用。方法不應該提供行爲,而應該只是建立數據成員,例如,構造,析構,Initialize()
,Reset()
, Validate()
。
如果你還需要更多的功能,用class會更恰當。如果不確定,那就用class吧。
注:struct和class的成員變量使用不同的命名規則。
4.6 繼承
組合往往比繼承更合適。使用public
繼承。
Ø 定義
當子類從基類繼承是,它引入了父基類定義的所有數據和操作的所有定義。實踐中,繼承在C++中的使用主要有兩種方式:實現繼承,也就是子類完全繼承所有代碼,另一種是接口繼承,只繼承方法名。
Ø 正面的
實現繼承能減少代碼量,它重用基類的代碼,這些代碼特化了現有的類型。因爲繼承是編譯期聲明的,你和編譯器可以理解這種操作,並探測錯誤。藉口繼承能迫使類暴露特定藉口API。同時,在這種情況下,如果這個類沒有定義API的必要的方法,編譯器能發現錯誤。
Ø 反面的
對於實現繼承,因爲子類的實現代碼是在基類和子類間傳播的,所以,可能會很難理解某個實現。子類不能重載非虛函數,所以子類不能改變實現。子類可能也定一了一些成員數據,因而指定了基類的物理邊界。
Ø 結論
繼承都應該用public
。如果你想要私有繼承(private
),你可以通過包含一個基類的實例的成員來實現。
不要過渡使用實現繼承。組合往往更合適。試着限制使用繼承”is-a”的情況:Bar繼承自Foo,除非Bar 是一種Foo看起來很合理。
儘量把析構函數設爲virtual
。 如果你的類裏有虛函數,那麼析構函數就應該是virtual
。注,數據成員應該是private
的。
限制protected
的使用,對那些可能需要從子類中訪問的成員函數。
重新定義繼承來的虛函數時,在派生類的聲明中,顯式聲明virtual
。原理:如果virtual
被忽略了,讀程序的人就不得不察看所有的祖先類,來確定這個函數到底是不是virtual
的疑惑。
4.7 多重繼承
只有很少的多重實現繼承是真正有用的。我們只在一種情況下允許多重繼承:基類中只有一個有實現,其它的基類都是純接口,並被標上了interface的後綴。
Ø 定義:
多重繼承允許子類有多個基類。我們把基類分爲純接口的基類和有實現的基類。
Ø 正面的:
多重實現繼承可以讓你重用比單繼承要來的多的代碼(請看繼承章節)。
Ø 反面的:
只有很少的多重實現繼承是真正有用的。當多重實現繼承看起來像是解決方案時,你往往可以找到另一種,更直率的,更清楚地解決方案。
Ø 結論:
只在這種情況下用多重繼承,所有的超類(除了一個例外—可能有一個是實現)都是純接口。爲了確保它們都是純接口,它們的名字都應該以interface的後綴結束。
注:在Windows上有一個此規則的例外。
4.8 接口
滿足了一定條件的類就可以,但不是必須的,就可以具有interface後綴。
Ø 定義:
如果一個類滿足了以下條件,那就是純接口:
l 它只有public的純虛方法(“=0”)和警惕方法(對析構函數請往下看)。
l 它可能沒有非靜態(non-static)的數據成員。
l 它不需要定義任何構造函數。如果提供了一個構造函數,必須是沒有參數的,並且必須是protected。
l 如果是一個子類,它只能從滿足這些條件的,並標記有interface後綴的類派生。
一個接口類永遠不能直接實例化,這是由於它定義的純接口方法。爲了確保接口的所有實現們都能被正確的銷燬,它們必須聲明一個virtual析構函數(對第一個條件的例外,這個不應該是純虛方法)。細節請參考Stroustrup的《C++編程語言 第三版,12.4章節》
Ø 正面的:
把一個類標記上interface的後綴,能讓別人知道他們不能添加實現方法和非靜態的數據成員。這在多重繼承的情況下是相當重要的。另外,接口語義在Java語言中早已是衆所周知的了。
Ø 反面的:
Interface後綴加長了類名,有可能會造成閱讀和理解的困難。同時,接口屬性可能會考慮到那些不應該暴露給客戶的實現細節。
Ø 結論:
僅當一個類滿足了上面的那些條件的時候,纔給它加上interface的後綴。然而,滿足上面調解的類也不是必須加上interface後綴。
4.9 操作符重載
不要重載操作符,除了在極少的,特殊的情況下。
Ø 定義:
類可以在類上定義像+和/這樣的操作符,就像那些內建類型一樣。
Ø 正面的:
能使代碼更直觀,因爲這樣的類將像具有跟內建類型(如int)一樣的行爲。重載的操作符往往是對於那些不具有豐富命名的函數的更頑皮的名字,比如,Eqauals()或者Add()。對於有些模板,爲了讓它們正確運行,你可能就需要定義一些操作符。
Ø 反面的:
雖然操作符重載能使代碼看起來更直觀,但它也有一些缺點:
l 它可能愚弄我們的直覺,使我們覺得代價高昂的操作是低廉的,內建的操作。
l 爲重載的操作符找到調用點是非常困難的。而找Equals()要比找==操作符的相關調用方便的多。
l 有些操作符還能對指針工作,這就更容易引入bug了。Foo + 4可能做一件事,而&Foo + 4 做的卻是完全不同的事。編譯器不會對這兩種情況有什麼抱怨,這就使debug變得非常困難。
重載也有令人驚奇的衍生物。比如,如果一個類重載了非數組的operator&,那它就不能被安全的前置聲明。
Ø 結論:
一般情況下,不要重載操作符。特別是複製操作符(operator=),非常隱蔽,應該被避免使用。如果需要,你可以定義像Equals()和CopyFrom()的函數。同樣地,如果這個類有可能被前置聲明,那就極力避免非常危險的非數組的operator&。
然而,可能有一些少數情況,爲了能跟模板或者標準C++類正確工作(比如爲了記日誌operator(ostream&, const T& )),你需要重載操作符。如果證明合法,這些是可接受的,但你最好儘可能避免它們。特別是,不要只是爲了使你的類能作爲STL容器的鍵值,去重載operator==或者operator<;你可以在聲明容器時,創建等值和比較的函數對象。
有些STL的算法確實要求你必須重載operator==,當這種情況下,你不得不這麼做時,在文檔中記錄下原因。
你還可以在看看拷貝構造函數和函數重載章節。
4.10 訪問控制
把數據成員都設成private,爲它們提供訪問函數(因爲技術上的原因,在使用Google Test時,我們允許測試裝置的類的數據成員爲protected)。典型情況下,一個變量可能叫foo_,訪問函數就叫foo(),你可能需要一個設值函數set_foo()。例外:static const(靜態常量)數據成員不用是private。
訪問函數往往在頭文件中定義成內聯的(inline)。
你也可以查看繼承和函數名的相關內容。
4.12 聲明順序
在類內使用特定的聲明順序:public:在private:之前,方法在數據成員之前,等等。
你的類定義應該以它的public:部分開始,後面跟着是protected:部分,然後是private:部分。如果某個部分是空的,那就省略它。
在每個部分內的聲明一般應該遵循以下的順序:
l Typedef和Enum
l 常量(static const—靜態常量數據成員)
l 構造函數
l 析構函數
l 方法,包括靜態方法
l 數據成員(除了static const—靜態常量數據成員)
友元聲明總是應該放在private部分,DISALLOW_COPY_AND_ASSIGN宏應該放在private:部分的最後。它應該是類的最後的東西。請看拷貝構造函數章節。
在相應的.cc文件中,方法定義應該跟聲明的順序儘可能一樣。
不要在類的定義中放入非常大的內聯方法的定義。通常,只有那些不重要的,性能要求高的,非常短的方法纔可能會定義成內聯的。更多詳情請看內聯函數章節。
4.13 寫短函數
選擇小的並且功能集中的函數。
我們發現,有時候很長的函數是合適的,所以在函數長短上沒有固定的限制。如果函數超過了40行,考慮一下它是否能被分成小函數,同時又不影響程序的結構。
即使你的長函數現在工作的非常好,別人在未來的什麼時候修改它,可能添加一些新行爲。這可能會帶來一些很難發現的bug。保持你的函數短小精悍,能幫助別人更容易的去閱讀和修改你的代碼。
你可以在你工作的代碼中找到一些很長的,並且很複雜的函數。不要試圖去修改已有的代碼:如果使用這樣的函數是非常困難的,或者你發現很難去debug錯誤,或者你想在幾個不同的上下文中使用它的一個片段,考慮把這樣的函數分割成小的,更有意義的片段。
5 Google 特有的魅力
我們有許多各種各樣的技巧和工具來使我們的代碼更健壯,還有許多我們使用C++的方法可能跟你在其它地方看到的很不一樣。
5.1 智能指針
如果你真的需要指針語義,scoped_ptr是非常棒的。在所有的特定情況下,你都應該只適用std::tr1::shared_ptr,比如當你需要把對象放在STL容器中存儲時。永遠不要使用auto_ptr。
智能指針是一些對象,它們看起來像指針,但被加入了一些其它語義。當一個scoped_ptr被銷燬時,比如,它刪除它指向的對象。shared_ptr也這樣,但它實現了引用計數,所以只有最後一個指向對象的指針去刪除那個對象。
一般來講,我們傾向於代碼設計的具有非常清晰的所有權。對象所有權最清晰的是,直接使用對象作爲字段或者局部變量,而根本不用指針。在另一種極端情況下,由於他們的每個定義,具有引用計數的指針不被任何人擁有。這種設計的問題是,很容易生成環形引用,或者另一種奇怪的情形—造成一個對象永遠不被銷燬。它也使每次進行的值拷貝或賦值的原子操作變慢。
雖然,並不是推薦的,但是具有引用計數的指針有時往往是最簡單,最幽雅的解決問題的方法。
5.2 cpplint
使用cplint.py探測類型錯誤。
cpplint.py是一個工具,它可以讀源文件,並且辨識許多類型錯誤。它雖然不是完美的,並且還有誤報(錯誤的判定—正確的和錯誤的),但它仍然是一個非常有意義的工具。可以在行尾加上//NOLINT的註釋來忽略正確的誤報。
有些項目有怎麼從項目工具中運行cpplint.py的說明。如果你出力的項目沒有,你可以單獨下載cpplint.py。
6 其他的C++特徵
6.1 引用參數