c++ effective總結(一)
條款一:視c++為一個語言聯邦
c++可以認為是由C,Object-Oriented C++(面向物件),Template C++(模板),STL(c++標準模板庫)四種次語言組成的。
條款二:儘量以const,enum,inline替換#define
c++中推薦使用其他的方法替換一些巨集定義操作,如常量定義,推薦使用const int MAX = 10 替換#define MAX 10。而對於#define定義的函式,也推薦使用inline(行內函數)替換。另外如在類中要宣告成員屬性的值。必須宣告成靜態常量,或者使用enum。如
class Stack{ public: ...private: static const int size = 10; // 或者 enum { size = 10 }; int members[size]; }
條款三:儘可能使用const
const在c++裡面可以說是用的非常多的一個修飾詞,const可以修飾值,指標,函式,函式引數,函式返回值等。
如const char* p = "hello"; 指標指向的值為常量。char* const p = "hello";指向值的指標為常量。可以記為const在*左邊值為常量,const在*右邊指標為常量。如果*兩邊都有const,則值和指標都為常量。
當你在使用stl容器時,使用迭代器訪問容器中的元素,而又不希望通過迭代器修改容器中的值,可以使用const_iterator,或者是遍歷常量容器物件,使用const_iterator。
const修飾成員函式,該成員函式不可以修改成員屬性值,另外const常用來傳遞的引用引數,當你不希望修改引用引數中的內容時,可以用const修飾。
條款四:確定物件被使用前已先被初始化
為內建物件進行手工初始化,因為c++不保證初始化它們,即你想初始化一個int物件,最好將int x;改寫成int x = 0;當然大部分情況下c++會為int x;分配 x = 0。
建構函式初始化成員變數時,最好使用成員初值列,這樣效率比賦值要高一些,尤其是對非內建物件,內建物件兩者沒影響。如
//推薦使用 class Student{ public: Student(string& name, intage):name_(name),age_(age) {}; private: string name_; int age_; } //不推薦 class Student{ public: Student(string& name, int age) { name_ = name; age_ = age; }; private: string name_; int age_; }
上面第一種只有對於string物件只有copy操作,而第二種既有copy操作,還有賦值操作。
條款五:瞭解c++默默編寫並呼叫哪些函式
你定義一個空類,編譯器會為它分配default建構函式,copy建構函式,copy assignment操作符合一個解構函式。如
class Empty {};
實際上上面的類等價於
class Empty { public: Empty() {...}; Empty (const Empty& e) {...}; ~Empty() {...}; Empty& operator=(const Empty& e) {...}; }
如果你自己定義了建構函式,copy建構函式,copy assignment,解構函式,則編譯器不會再分配這些函式。
編譯器分配的copy assignment有時候存在問題,如string& name這樣的成員屬性,編譯器分配的copy assignment是不可以賦值的,你需要自己重新定義copy assignment。
條款六:若不想使用編譯器自動生成的函式,就該明確拒絕
如條款五所說編譯器會自動分配如copy建構函式,copy assignment這類的函式,如果你不想要這些函式,可以在private下宣告這些函式就行了。如
class Student{ private: Student (const Student&); Student& operator=(const Student&); }
條款七:為多型基類宣告virtual解構函式
帶有多型性質的基類,或者類中有帶vitural的成員函式,則其解構函式都應該定義為virtual解構函式。但如果該類不是設計為基類,則不要宣告virtual解構函式。如果基類中的解構函式沒有定義為vitural,則會出現一些問題,如在工廠模式中。有以下程式碼
class Person{ public: Person(); ~Person(); } class Student : public Person {...}; Person* getPerson(); Person* student = getPerson();
delete student;
假設當前的getPerson()獲取一個派生類Student物件,當使用完之後delete student時因為指標是基類指標,所以會銷燬基類中的成員,但是不會銷燬派生類中的成員,所以會存在”部分銷燬“的現象,但是若基類中的解構函式是vitural,則會刪除派生類中的成員。
另外如果一個類不是基類,不要宣告vitural函式,因為vitural會引入虛表指標和虛表,會佔用一部分記憶體。
條款八:別讓異常逃離解構函式
這種通常指在解構函式中呼叫了一些函式,而這些函式可能引入異常,此時需要做一些處理,儘量保證不要讓解構函式的異常傳遞出來,或者說盡量確保解構函式中不要發生異常。
條款九:絕對不要在建構函式和解構函式中呼叫virtual函式
因為上述這類呼叫絕對不會下降至下一層,即派生層,這種情況避免就好了。一般也不會這樣去呼叫
條款十:令operator= 返回一個reference to *this
這是一種固定的assignment 操作符寫法,如
class Student{ public: Student& operator=(const Student& s){ name_ = s.name_; age_ = s.age_; return *this; } private: string name_; int age_; }
除了operator= 如operator+=等都可以寫成這樣。
條款十一:在operator= 中處理”自我賦值“
自我賦值,即a = a;當然一般這種情況不會發生,但是這種*a = *b,而指標a和b都指向同一個值,這樣就存在問題。如果operator= 是下面這種寫法
class Bitmap {}; class Widget { public: Widget& operator=(const Widget& rhs){ delete pb; pb = new Bitmap(*rhs.pb); return *this; } private: Bitmap* pb; }
如果rhs == this,則delete pb時也刪除了rhs中的pb。所以可以改寫成
class Bitmap {}; class Widget { public: Widget& operator=(const Widget& rhs){ Bitmap* pOrig = pb; pb = new Bitmap(*rhs.pb); delete pOrig; return *this; } private: Bitmap* pb; }
條款十二:複製物件時勿忘其每一個成分
copy建構函式和copy assignment函式中不要忘了每一個成員變數,忘記了編譯器也不會報錯。如
class Student{ public: Student (const Student& s):name_(s.name_) {}; private: string name_; int age_; }
派生類中的copy建構函式也不要忘了基類中的成員變數,可以直接呼叫基類的建構函式。
class Person{ public: Person (const Person& p):name_(p.name_), age_(p.age_) {}; private: string name_; int age_; } class Student : public Person { public: Student (const Student& s) : Person(s), grade_(s.grade_){}; private: int grade_; }
條款十三:以物件管理資源
在c++中new和delete必須是同時存在的,但很多時候會忘記delete,或者說你很仔細的沒有忘記delete,但是在new和delete之間的程式碼可能會存在return,continue等這類操作,而跳過了delete,為了防止這種記憶體洩漏的情況發生,所以也就有了以物件管理資源,當物件唄釋放時,物件的解構函式會自動釋放這些資源,如auto_ptr就是這種的資源管理物件。而c++11中的unique_ptr,shared_ptr也是這一類物件,只不過unique_ptr不能多個物件共享一塊記憶體,而shared_ptr通過引用計數機制,可以多個物件共享一塊記憶體,只有當引用計數為0才會釋放記憶體。
條款十四:在資源管理類中小心copying行為
RAII(Resource Acquisition Is Initailization,資源取得時便是初始化時,也是以物件管理資源的概念)物件複製時要一併複製它所管理的資源。而普遍常見的RAII class copying行為是:抑制copying,施行引用計數法等。
條款十五:在資源管理類中提供對原始資源的訪問
APIs中往往需要訪問原始資源,所以每一個RAII class應該提供一個”取得其所管理之資源“的方法,如get成員函式可以獲得原始指標,過載了指標取值運算子,轉換到原始指標並取值。
對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯式轉換比較安全,但隱式轉換對客戶比較方便。
條款十六:成對使用new和delete時要採取相同形式
如下例子
string* stringArray = new string[100]; ... delete stringArray;
上面的new和delete並不是一個很好的應用,因為new出來的是一個數組,而delete很可能只釋放了一個元素,標準的用法是
string* stringArray = new string[100]; ... delete [] stringArray;
所以說new和delete,new [] 和delete []。針對此問題要注意typedef的使用,最好不要對陣列使用typedef,否則很容易在delete時忘了帶[]。
條款十七:以獨立語句將newed物件置入智慧指標
以獨立語句將newed物件儲存於智慧指標內。如果不這樣做,一旦異常丟擲,有可能導致難以察覺的資源洩漏。如
int priority(); void processWidget(std::shared_ptr<Widget> pw, int priority); //呼叫 processWidget(std::shared_ptr<Widget> (new Widget), priority());
在c++中上述執行的順序並不嚴格,可能先執行new Widget,再執行priority(),最後才執行shared_ptr的呼叫。這樣一旦priority()呼叫報錯,就不會發生new Widget的記憶體洩漏。所以最好獨立語句將物件放入智慧指標。
std::shared_ptr<Widget> pw(new Widget); processWidget(pw, priority());
條款十八:讓介面容易被正確使用,不易被誤用
總結成一句話就是在設計介面時,儘量簡潔明瞭,約束性強,不容易發生誤用。
條款十九:設計class猶如設計type
條款二十:寧以pass-by-reference-to-const替換pass-by-value
在函式的引數傳遞過程中對於非內建型別物件,儘量以引用或指標傳遞,推薦引用,為了避免函式中修改傳遞的物件,可以加上const修飾符。對於非內建型別,引用的傳遞要比傳值高效很多,因為傳值的過程中相當於傳遞副本,是需要呼叫copy建構函式,而函式執行完之後還會呼叫解構函式銷燬copy的物件,而引用傳遞不存在這一問題。
條款二十一:必須返回物件時,別妄想返回其reference
在函式的返回值時切勿返回reference、pointer,尤其是指向函式中的區域性物件,直接返回值即可,雖然這樣會耗時(呼叫建構函式),但至少是正確的。
條款二十二:將成員變數宣告為private
條款二十三:寧以non-member,non-friend替換member函式
條款二十四:若所有引數皆需型別轉換,請為此採用non-member函式
條款二十五:考慮寫出一個不拋異常的swap函式
條款二十六:儘可能延後變數定義式的出現時間
定義的變數即使沒有被使用,也會存在構造和析構的操作,也就是或會存在構造和析構的成本,所以在定義變數時儘量在使用時定義。
條款二十七:儘量少做轉型動作
如果可以,儘量避免轉型,特別是在注重效率的程式碼中避免dynamic_casts。
如果轉型是必要的,試著將他隱藏於某個函式背後。客戶隨後可以呼叫該函式,而不需將轉型放入到他們自己的程式碼內。
寧可使用c++風格的新式轉型,不要使用舊式轉型。
條款二十八:避免返回handles指向物件內部成分
對於下面的例子,返回引用(handles,指標,迭代器也可以歸為這一類)會導致內部資料的不安全.
class Point { public: Point(int x, int y); void setX(int newVal); void setY(int newVal); } struct RectData{ Point ulhc; Point lrhc; } class Rectangle{ public: Point& upperLeft () const {return pData->ulhc}; Point& lowerRight () const {return pData->lrhc}; private: std::shared_ptr<RectData> pData; }
上面的Rectangle類中,雖然upperLeft函式是const函式,但是返回的結果是Point的引用,通過該引用是可以修改Point內部的值,包括返回指標和迭代器都是會發生類似的情況,所以如果硬要返回引用這類handles,可以返回const references
class Rectangle{ public: const Point& upperLeft () const {return pData->ulhc}; const Point& lowerRight () const {return pData->lrhc}; private: std::shared_ptr<RectData> pData; }
條款二十九:為”異常安全“而努力是值得的
條款三十:透徹瞭解inlining的裡裡外外
總之只有在小型且頻繁被呼叫的函式身上才使用inlining。這樣可以保證程式碼膨脹的問題最小化,程式的速度提升最大化,如max函式。
條款三十一:將檔案間的編譯依存關係降至最低
條款三十二:確定你的public繼承塑模出is-a關係
基類和派生類之間一定要有is-a關係,即基類的屬性和方法,在派生類中一定是合理存在的。
條款三十三:避免遮掩繼承而來的名稱
只要熟悉作用域,根據作用域去定義變數就可以了,知道在使用變數時作用域的先後順序。
條款三十四:區分介面繼承和實現繼承
這裡涉及到基類中的純虛擬函式,虛擬函式和非虛擬函式以及繼承的概念。
純虛擬函式:含有純虛擬函式的類是抽象類,不可以被new出來。對於純虛擬函式,子類只繼承了介面,子類中必須宣告和實現純虛擬函式,當然父類中也可以定義純虛擬函式,但要指定父類名稱才可以呼叫。
虛擬函式:子類會繼承虛擬函式的介面和預設實現,子類中也可以重寫虛擬函式。
非虛擬函式:子類會繼承非虛擬函式的介面和強制實現,不建議子類中重寫非虛擬函式。如果硬要重寫,呼叫時基類指標會呼叫基類中的函式,想呼叫子類中的重寫函式,必須使用子類指標。
條款三十五:考慮virtual函式以外的其他選擇
在子類繼承父類的功能時,可以引入一些設計模式來使得繼承更靈活。如使用模板模式,策略模式。
條款三十六:絕不重新定義繼承而來的非虛擬函式
正如上面所說如果子類重寫了父類的非虛擬函式,父類指標即使是指向子類,當呼叫非虛擬函式時,還是會呼叫父類的非虛擬函式,所以非虛擬函式的呼叫和指標型別繫結。所以為了避免出現這樣的迷惑行為,還是不要重寫非虛擬函式。
條款三十七:絕不重新定義繼承而來的預設引數值
對於繼承而來的虛擬函式或純虛擬函式,如果函式中有定義的預設引數值,繼承時不要修改預設引數值,因為預設引數值是靜態繫結(發生在編譯期)的。而虛擬函式或純虛擬函式時動態繫結(發生在執行期)的。
條款三十八:通過複合塑模出has-a或”根據某物實現出“
條款三十九:明智而審慎地使用private繼承
private並不會繼承介面,而是繼承基類的實現,總之儘量使用public,除非特殊實現上,因為private繼承本質上不屬於is-a關係。
條款四十:明智而審慎地使用多重繼承
總之能使用單一繼承的儘量使用單一繼承,多重繼承太過複雜,容易引入歧義,且應用於多重繼承的虛繼承既要提前設計也會引入大小、速度等成本。
條款四十一:瞭解隱式介面和編譯器多型
c++中的模板創造了隱式介面和編譯期的多型。
條款四十二:瞭解typename的雙重意義
在宣告模板時,template<typename T> 和 template<class T>的含義是一樣的,但是typename可以表示巢狀從屬型別,如T::setX到底是一個型別還是成員名稱,如果加上typename就很明確是一個型別了,typename T::setX。