程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> Effective C++ 55個條款

Effective C++ 55個條款

編輯:關於C++

讓自己習慣C++

視C++為一個語言聯邦

C++高效編程守則視狀況而變化,取決於你使用C++的哪一部分。

盡量以const,enums,inline替換#define

對於單純常量,最好以const對象或enums替換#defines;

對於形似函數的宏(macros),最好改用inline函數替換#defines。

盡可能使用const

將某些東西聲明為const可幫助編譯器偵測出錯誤用法。const可被施加於任何作用域內的對象、函數參數、函數返回類型、成員函數本體;

編譯器強制實施bitwise constness,當你編寫程序時應該使用“概念上的常量性”(conceptual constness)

當const和non-const成員函數有著實質等價的實現時,令non-const版本調用const版本可避免代碼重復。

確定對象被使用前已先被初始化

為內置性對象進行手工初始化,因為C++不保證初始化它們;

構造函數最好使用成員初值列(member initialization list),而不要在構造函數本體內使用賦值操作(assignment)。初值列列出的成員變量,其排列次序應該和它們在class中的聲明次序相同;

為免除“跨編譯單元值初始化次序”問題,輕易local static對象替換non-local static對象。

構造/析構/賦值運算

了解C++默默編寫並調用哪些函數

編譯器可以暗自為class創建default構造函數、copy構造函數、copy assignment操作符,以及析構函數。

若不想使用編譯器自動生成的函數,就該明確拒絕

為駁回編譯器自動(暗自)提供的機能,可將相應的成員函數聲明為private並且不予以實現。

為多態基類聲明virtual析構函數

polymorphic(帶多態性質的)base classes應該聲明一個virtual析構函數。如果class帶有任何virtual函數,它就應該擁有一個virtual析構函數;

Classes的設計目的如果不是作為base classes使用,或不是為了具備多態性(polymorphically),就不該聲明virtual析構函數。

別讓異常逃離析構函數

析構函數絕對不要吐出異常。如果一個被析構函數調用的函數可能拋出異常,析構函數應該捕獲任何異常,然後吐下它們(不傳播)或結束程序;

如果客戶需要對某個操作函數運行期間拋出的異常做出反應,那麼class應該提供一個普通函數(而非在析構函數中)執行該操作。

絕不在構造和析構過程中調用virtual函數

在構造和析構期間不要調用virtual函數,因為這類調用從不下降至derived class(比起當前執行構造函數和析構函數的那層)。

令operator=返回一個reference to *this

令賦值(assignment)操作符返回一個reference to *this。

在operator=中處理“自我賦值”

確保當對象自我賦值時operator=有良好行為。其中技術包括比較“來源圖像”和“目標對象”的地址、精心周到的語句順序、以及copy-and-swap;

確定任何函數如果操作一個以上的對象,而其中多個對象時同一個對象時,其行為仍然正確。

復制對象時勿忘其每一個成分

Copying函數應該確保復制“對象內的所有成員變量”及“所有base class成分”;

不要嘗試以某個copying函數實現另一個copying函數。應該講共同機能放進第三個函數中,並由兩個copying函數共同調用。

資源管理

以對象管理資源

為防止資源洩露,請使用RAII對象,它們在構造函數中獲得資源並在析構函數中釋放資源;

兩個常常被使用的RAII classes分別是tr1::shared_ptr和auto_ptr。前者通常是較佳選擇,因為其copy行為比較直觀。若選擇auto_ptr,復制動作會使它(被復制物)指向null。

在資源管理類中小心copying行為

復制RAII對象必須一並復制它所管理的資源,所以資源的copying行為決定RAII對象的copying行為;

普遍而常見的RAII class copying行為是:抑制copying、施行引入計數法(reference counting)。不過其他行為也都可能被實現。

在資源管理類中提供對原始資源的訪問

APIs往往要求訪問原始資源(raw resources),所以每一個RAII class應該提供一個“取得其所管理之資源”的辦法;

對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯示轉換比較安全,但隱式轉換對客戶比較方便。

成對使用new和delete時要采取相同的形式

如果你在new表達式中使用[],必須在相應的delete表達式中也是用[]。如果你在new表達式中不使用[],一定不要在相應的delete表達式中使用[]。

以獨立語句將newed對象置入智能指針

以獨立語句將newed對象存儲於(置入)智能指針內。如果不這樣做,一旦異常被拋出,有可能導致難以察覺的資源洩露。

設計與聲明

讓接口容易被正確使用,不容易被誤用

好的接口很容易被正確使用,不容易被誤用。你應該在你的所有接口中努力達成這些性質;

“阻止誤用”的辦法是建立新類型、限制類型上的操作,束縛對象值,以及消除客戶的資源管理任務;

tr1::shared_ptr支持定制型刪除器(custom deleter)。這可防范DLL問題,可被用來自動解除互斥鎖(mutexes)等等。

設計class猶如設計type

Class的設計就是type的設計。在定義一個新type之前,請確定考慮新type對象的創建和銷毀、對象的初始化和賦值、合法值、繼承關系等。

寧以pass-by-reference-to-const替換pass-by-value

盡量以pass-by-reference-to-const替換pass-by-value。前者通常比較高效,並可避免切割問題;

以上規則並不適用於內置類型,以及STL的迭代器和函數對象。對它們而言,pass-by-value往往比較適當。

必須返回對象時,別妄想返回其reference

絕不要返回pointer或reference指向一個local stack對象,或返回reference指向一個heap-allocated對象,或返回pointer或reference指向一個local static對象而有可能同時需要多個這樣的對象。

將成員變量聲明為private

切記將成員變量聲明為private。這可賦予客戶訪問數據的一致性、可細微劃分訪問控制、允許約束條件獲得保證,並提供class作者以充分的實現彈性;

protected並不比public更具封裝性。

寧以non-member、non-friend替換member函數

寧可拿non-member non-friend函數替換member函數。這樣做可以增加封裝性、包裹彈性(packaging flexibility)和機能擴充性。

若所有參數皆需類型轉換,請以此采用non-member函數

如果你需要為某個函數的所有參數(包括被this指針所指的那個隱喻參數)進行類型轉換,那麼這個函數必須是個non-member。

考慮寫出一個不拋出異常的swap函數

當std::swap對你的類型效率不高時,提供一個swap成員函數,並確定這個函數不拋出異常;

如果你提供一個member swap,也該提供一個non-member swap用來調用前者。對於classes(而非templates),也請特化std::swap;

調用swap時應針對std::swap使用using聲明式,然後調用swap並且不帶任何“命名空間資格修飾”;

為“用戶定義類型”進行std templates全特化是好的,但千萬不要嘗試在std內加入某些對std而言全新的東西。

實現

盡可能延後變量定義式的出現時間

盡可能延後變量定義式的出現。這樣做可增加程序的清晰度並改善程序效率。

盡量少做轉型動作

如果可以,盡量避免轉型,特別是在注重效率的代碼中避免dynamic_casts。如果有個設計需要轉型動作,試著發展無需轉型的替代設計;

如果轉型是必要的,試著將它隱藏於某個函數背後。客戶隨後可以調用該函數,而不需要將轉型放進他們自己的代碼內;

寧可使用C++ style(新式)轉型,不要使用舊式轉型。前者很容易辨識出來,而且也比較有著分門別類的職掌。

避免返回handles指向對象內部成分

避免返回handles(包括references、指針、迭代器)指向對象內部。遵守這個條款可增加封裝性,幫助const成員函數的行為像個const,並將發生“虛吊號碼牌”的可能性降至最低。

為“異常安全”而努力是值得的

異常安全函數(Exception-safe functions)即使發生異常也不會洩露資源或允許任何數據結構敗壞。這樣的函數區分為三種可能的保證:基本型、強烈型、不拋異常型;

“強烈保證”往往能夠以copy-and-swap實現出來,但“強烈保證”並非對所有函數都可實現或具備現實意義;

函數提供的“異常安全保證”通常最高只等於其所調用之各個函數的“異常安全保證”中的最弱者。

透徹了解inlining的裡裡外外

將大多數inlining限制在小型、被頻繁調用的函數身上。這可使日後的調試過程和二進制升級(binary upgradability)更容易,也可使潛在的代碼膨脹問題最小化,使程序的速度提升機會最大化;

不要只因為function templates出現在頭文件,就將它們聲明為inline。

將文件間的編譯依存關系降至最低

支持“編譯依存性最小化”的一般構想是:相依於聲明式,不要相依於定義式。基於此構想的兩個手段是Handle classes和Interface classes;

程序庫頭文件應該以“完全且僅有聲明式”(full and declaration-only forms)的形式存在。這種做法不論是否涉及templates都適用。

繼承與面向對象設計

確定你的public繼承塑模出is-a關系

“public繼承”意味is-a。適用於base classes身上的每一件事情一定也適用於derived classes身上,因為每一個derived class對象也都是一個base class對象。

避免遮蔽繼承而來的名稱

derived classes內的名稱會遮掩base classes內的名稱。在public繼承下從來沒有人希望如此;

為了讓被遮掩的名稱再見天日,可使用using聲明式或轉交函數(forwarding functions)。

區分接口繼承和實現繼承

接口繼承和實現繼承不同。在public繼承之下,derived classes總是繼承base class的接口;

pure virtual函數只具體指定接口繼承;

簡樸的(非純)impure virtual函數具體指定接口繼承及缺省實現繼承;

non-virtual函數具體指定接口繼承以及強制性實現繼承。

考慮virtual函數以外的其他選擇

virtual函數的替代方案包括NVI手法及Strategy設計模式的多種形式。NVI手法自身是一個特殊形式的Template Method設計模式;

將機能從成員函數移到class外部函數,帶來的一個缺點是,非成員函數無法訪問class的non-public成員;

tr1::function對象的行為就像一般函數指針。這樣的對象可接納“與給定之目標簽名式(target signature)兼容”的所有可調用物(callable entities)。

絕不重定義繼承而來的non-virtual函數

絕不要重新定義繼承而來的non-virtual函數。

絕不重新定義繼承而來的缺省參數值

絕對不要重新定義一個繼承而來的缺省參數值,因為缺省參數值都是靜態綁定,而virtual函數——你唯一應該覆寫的東西——卻是動態綁定。

通過復合塑模出has-a或“根據某物出現”

復合(composition)的意義和public繼承完全不同;

在應用域(application domain),復合意味has-a(有一個)。在實現域(implementation domain),復合意味著is-implemented-in-terms-of(根據某物實現出)。

明智而審慎地使用private繼承

Private繼承意味著is-implement-in-terms-of(根據某物實現出)。它通常比復合(composition)的級別低。但是當derived class需要訪問protected base class的成員,或需要重新定義繼承而來的virtual函數時,這麼設計是合理的;

和復合(composition)不同,private繼承可以造成empty base最優化。這對致力於“對象尺寸最小化”的程序開發者而言,可能很重要。

明智而審慎地使用多重繼承

多重繼承比單一繼承復雜。它可能導致新的歧義性,以及對virtual繼承的需要;

virtual繼承會增加大小、速度、初始化(及賦值)復雜度等等成本。如果virtual base classes不帶任何數據,將是最具實用價值的情況;

多重繼承的確有正當用途。其中一個情節涉及“public繼承某個Interface class”和“private繼承某個協助實現的class”的兩相結合。

模板與泛型編程

了解隱式接口和編譯器多態

classes和template都支持接口(interfaces)和多態(polymorphism);

對classes而言接口是顯示的(explicit),以函數簽名為中心。多態則是通過virtual函數發生於運行期;

對templates參數而言,接口是隱式的(implicit),奠基與有效表達式。對態則是通過template具現化和函數重載解析(function overloading resolution)發生於編譯期。

了解typename的雙重意義

聲明template參數時,前綴關鍵字class和typename可互換;

請使用關鍵字typename標識嵌套從屬類型名稱;但不得在base class lists(基類列)或member initialization list(成員初值列)內以它作為base class修飾符。

學習處理模板化基類內的名稱

可在derived class templates內通過“this->”指涉base class templates內的成員名稱,或籍由一個明白寫出的“base class資格修飾符”完成。

將與參數無關的代碼抽離

Templates生成多個classes和多個函數,所以任何template代碼都不該與某個造成膨脹的template參數產生相依關系;

因非類型模板參數(non-type template parameters)而造成的代碼膨脹,往往可消除,做法是以函數參數或class成員變量替換template參數;

因類型參數(type parameters)而造成的代碼膨脹,往往可以降低,做法是讓帶有完全相同二進制表示(binary representations)的具現類型(instantiation types)共享實現碼。

運用成員函數模板接受所有兼容類型

請使用member function templates(成員函數模板)生成“可接受所有兼容類型”的函數;

如果你聲明member templates用於“泛化copy構造”或“泛化assignment操作”,你還是需要聲明正常的copy構造函數和copy assignment操作符。

需要類型轉換時請為模板定義非成員函數

當我們編寫一個class template,而它所提供之“與此template相關的”函數支持“所有參數之隱式類型轉換”時,請將那些函數定義為“class template內部的friend函數”。

請使用traits classes表現類型信息

Traits classes使得“類型相關信息”在編譯期可用。它們以templates和“templates特化”完成實現;

整個重載技術(overloading)後,traits classes有可能在編譯期對類型執行if…else測試。

認識template元編程

Template metaprogramming(TMP,模板元編程)可將工作由運行期移往編譯期,因而得以實現早期錯誤偵測和更高的執行效率;

TMP可被用來生成“基於政策選擇組合”(based on combinations of pilicy choices)的客戶定制代碼,也可用來避免生成對某些特殊類型並不適合的代碼。

定制new和delete

了解new-handler的行為

set_new_handler允許客戶指定一個函數,在內存分配無法獲得滿足時被調用;

Nothrow new是一個頗為局限的工具,因為它只適用於內存分配;後繼的構造函數調用還是可能拋出異常。

了解new和delete的合理替換時機

有許多理由需要寫個自定的new和delete,包括改善效能、對heap運用錯誤進行調試、收集heap使用信息。

編寫new和delete時需固守常規

operator new應該內含一個無窮循環,並在其中嘗試分配內存,如果它無法滿足內存需求,就該調用new-handler。它也應該有能力處理0 bytes申請。Class專屬版本則還應該處理“比正確大小更大的(錯誤)申請;

operator delete應該在收到null指針時不做任何事。Class專屬版本則還應該處理“比正確大小更大的(錯誤)申請。

寫了placement new也要寫placement delete

當你寫一個placement operator new,請確定它也寫出了對應的placement operator delete。如果沒有這樣做,你的程序可能會發生隱微而時斷時續的內存洩露;

當聲明placement new和placement delete,請確定不要無意識(非故意)地遮蓋了它們的正常版本。

雜項討論

不要請勿編譯器的警告

嚴肅對待編譯器發生的警告信息。努力在你的編譯器的最高(最苛刻)警告級別下爭取“無任何警告”的榮譽;

不要過度依賴編譯器的報警功能,因為不同的編譯器對待事情的態度並不相同。一旦移植到另一個編譯器上,你原本依賴的警告信息有可能消失。

讓自己熟悉包括TR1在內的標准程序庫

C++標准程序庫的主要機能由STL、iostreams、locales組成。並包含C99標准程序庫;

TR1添加了只能指針(如tr1::shared_ptr)、一般化函數指針(tr1::function)、hash-based容器、正則表達式(regular expression)以及另外10個組件的支持;

TR1自身只是一份規范。為獲得TR1提供的好處,需要一份實物。一個好的實物來源是Boost。

讓自己熟悉Boost

Boost是一個社群,也是一個網站。致力於免費、源碼開放、同僚復審的C++程序庫開發。Boost在C++標准化過程中扮演具有影響力的角色;

Boost提供許多TR1組件實現品,以及其他許多程序庫。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved