條款01:視C++為一個語言聯邦
為了理解C++,你必須認識其主要的次語言。總共有四個:
(1)C
C++仍是以C為基礎。
(2)Object-Oriented C++
classes(類)(包括構造函數和析構函數),encapsulation(封裝),inheritance(繼承), polymorphism(多態),virtual
functions (dynamic binding)(虛擬函數(動態綁定))等。
(3)Template C++
這是C++的generic programming(泛型編程)部分
(4)STL
STL 是一個 template library(模板庫)。
C++並不是一個帶有一組守則的一體語言:它是四個次語言組成的聯邦政府,每個次語言都有自己的規約。記住這四個次語言你就會發現C++容易了解多了。
請記住:C++高效編程守則視狀況而變化,取決於你使用C++的哪一部分。
-------------------------------------------------------------------------------------------------------------------
條款02:盡量以const,enum,inline替換#define
該條款最好稱為:“盡量用編譯器而不用預處理”,因為#define不被視為語言的一部分。
#define ASPECT_RATIO 1.653
編譯器永遠也看不到ASPECT_RATIO這個符號名,因為在源碼進入編譯器之前,它會被預處理程序去掉,於是ASPECT_RATIO不會加入到符號列表中。
解決的辦法是:不用預處理宏,定義一個常量:
const double AspectRatio = 1.653; //大寫名稱通常用於宏,所以這裡改變名稱寫法
說明:
(1)作為語言常量,AspectRatio會被編譯器看到,記入符號表。
(2)使用常量可能比使用#define導致較小量的碼。因為預處理器盲目地用1.653置換ASPECT_RATIO導致目標代碼中存在多個1.653的拷貝。如果使用常量AspectRatio,就不會產生多於一個的拷貝。
要把常量限制在類中,首先要使它成為類的成員;為了保證常量最多只有一份拷貝,還要把它定義為靜態成員:
class GamePlayer { private: static const int NUM_TURNS = 5; // constant declaration int scores[NUM_TURNS]; // use of constant ... };
說明:
(1)語句是NUM_TURNS的聲明,而不是定義。
(2)C++要求對所使用的任何東西提供一個定義式。如果它是class專屬常量且為static整數類型(例如:ints,chars,bools等),只要不取它們的地址可以只聲明並使用而無須定義。
(3)NUM_TURNS的定義如下:
const int GamePlayer::NUM_TURNS;
由於class常量已在聲明時獲得初值,因此定義時可不設初值。
(4)沒有辦法使用#define來創建一個類屬常量,因為#defines不考慮作用域。一旦宏被定義,它就在其後編譯過程中有效(除非後面某處存在#undefed)。
舊一點的編譯器認為類的靜態成員在聲明時定義初始值是非法的。可以在定義賦值:
class EngineeringConstants // header file { private: static const double FUDGE_FACTOR; ... }; // this goes in the class implementation file const double EngineeringConstants::FUDGE_FACTOR = 1.35;
如果在編譯器需要FUDGE_FACTOR的值例如,作為數組維數,是不行的。因為編譯器必須在編譯器間知道數組的大小。可以用enum解決:
class GamePlayer { private: enum { NUM_TURNS = 5 }; int scores[NUM_TURNS]; };
說明:取一個enum地址是非法的。
一個普遍的#define指令的用法是用它來實現那些看起來像函數而又不會導致函數調用的宏。
#define max(a,b) ((a) > (b) ? (a) : (b))
注意:寫宏時要對每個參數都要加上括號,否則會造成調用麻煩。但也會造成下面的錯誤:
int a = 5, b = 0; max(++a, b); // a 的值增加了2次 max(++a, b+10); // a 的值只增加了1次
你可以用普通函數實現宏的效率,再加上可預計的行為和類型安全。
template<typename T> inline const T& max(const T& a, const T& b) { return a > b ? a : b; }
請記住:
(1)對於單純常量,最好以const或enums替換#defines。
(2)對於形似函數的宏,最好改用inline函數替換#defines。
-------------------------------------------------------------------------------------------------------------------
條款03:盡可能使用const
const允許你告訴編譯器某值保持不變,並獲得編譯器幫助,確保這條約束不被違反。在classes的外部,可以將它用於 global(全局)或namespace(命名空間)范圍的 constants(常量),或修飾文件、函數、或區塊作用域中被聲明為static對象。修飾classes內部的static和non-static成員。修飾指針自身,指針所指物。
char greeting[] = "Hello"; char *p = greeting; // non-const pointer, non-const data const char *p = greeting; // non-const pointer,const data char * const p = greeting; // const pointer,non-const data const char * const p = greeting; // const pointer,const data
說明:當指針指向的內容為常量時,const放在類型之前和類型之後意義相同。
void f1(const Widget *pw); // f1 takes a pointer to a constant Widget object void f2(Widget const *pw); // so does f2
對於STL迭代器來說:
const std::vector<int>:: iterator iter = vec.begin(); // iter acts like a T* const *iter = 10; // OK, changes what iter points to ++iter; // error! iter is const std::vector<int>:: const_iterator cIter = vec.begin(); // cIter acts like a const T* *cIter = 10; // error! *cIter is const ++cIter; // fine, changes cIter
const 成員函數
將const實施於成員函數的目的是確認該成員函數可作用於const對象身上。兩個函數如果只是常量性不同,可以被重載。
class TextBlock { public: ... const char& operator[] (std::size_t position) const // operator[] for const objects { return text[position]; } char& operator[] (std::size_t position) // operator[] for non-const objects { return text[position]; } private: std::string text; }; TextBlock tb("Hello"); const TextBlock ctb("World"); std::cout << tb[0]; // fine — reading a non-const TextBlock tb[0] = 'x'; // fine — writing a non-const TextBlock std::cout << ctb[0]; // fine — reading a const TextBlock ctb[0] = 'x'; // error! — writing a const TextBlock
為了避免重復,可以利用轉型修改代碼。根據const版本的operator[]實現其non-const版本。
class TextBlock { public: ... const char& operator[](std::size_t position) const { ... ... ... return text[position]; } char& operator[](std::size_t position) // now just calls const op[] { return const_cast<char&>( // cast away const on // op[]'s return type; static_cast<const TextBlock&>(*this) // add const to *this's type; [position] // call const version of op[] ); } ... };
注意:令const 版本調用non-const版本來避免重復是錯誤的。一個 const成員函數承諾絕不會改變它的邏輯狀態,但是一個non-const成員函數不會做這樣的承諾。從一個const成員函數調用一個non-const成員函數,將面臨承諾不會變化的對象被改變的風險。即const成員函數調用non-const成員函數是一種錯誤行為。
請記住:
(1)將某些東西聲明為const可幫助編譯器偵測出錯誤用法。const可被施加於任何作用域內的對象、函數參數、函數返回類型、成員函數本體。
(2)當const和non-const成員函數有著實質等價的實現時,令non-const版本調用const版本可避免代碼重復。
-------------------------------------------------------------------------------------------------------------------
條款04:確定對象被使用前已先被初始化
讀取一個未初始化的值會引起未定義行為。因此,永遠在使用對象之前先將它初始化。對於無任何成員的內置類型,你必須手工完成此事。
int x = 0; // manual initialization of an int const char * text = "A C-style string"; // manual initialization of a pointer double d; std::cin >> d; // "initialization" by reading from an input stream
對於內置類型以外的東西,由構造函數初始化,確保將對象的每一個成員都初始化。重要的是不要把賦值和初始化混淆。
class PhoneNumber { ... }; class ABEntry { // ABEntry = "Address Book Entry" public: ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones); private: std::string theName; std::string theAddress; std::list<PhoneNumber> thePhones; int num TimesConsulted; }; ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) { theName = name; // these are all assignments, theAddress = address; // not initializations thePhones = phones; numTimesConsulted = 0; }
這樣做雖然使得ABEntry對象具有了你所期待的值,但不是最好的做法。C++規定對象的成員變量的初始化動作發生在進入構造函數本體之前。效率較高的寫法是:
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) : theName(name) , theAddress(address) , // these are now all initializations thePhones(phones) , numTimesConsulted(0) {} // the ctor body is now empty
說明:
(1)版本1首先調用default構造函數為theName,theAddress和thePhones設初值,然後立即再對它們賦予新值。default構造函數的一切作為因此浪費了。
(2)版本2的成員初始化列表的做法避免了這個問題。初始化列表中針對各個成員變量而設的實參,被拿去作為各成員變量構造函數的實參。theName以name為初值進行拷貝構造,theAddress以address為初值進行拷貝構造, thePhones以phones為初值進行拷貝構造。
(3)對於大多數類型來說,只調用一次拷貝構造函數的效率比先調用一次default構造函數再調用一次copy assignment operator(拷貝賦值運算符)的效率要高(有時會高很多)。
(4)對於內置類型對象如numTimesConsulted,其初始化和賦值成本相同,但為了一致性最好也通過成員初始化列表來初始化。
(5)當想要構造一個default構造函數時,也可以使用成員初始化列表。
ABEntry::ABEntry() :theName() , // call theName's default ctor; theAddress() , // do the same for theAddress; thePhones() , // and for thePhones; numTimesConsulted(0) // but explicitly initialize {}
當然,編譯器會為用戶自定義類型成員變量自動調用default構造函數,如果那些成員變量沒有在成員初始化列表中被指定初值。
(6)如果成員變量是const或引用,即使內置類型也一定需要初值,不能被賦值。
(7)C++有固定的成員初始化次序:基類早於派生類,class成員變量總是以其聲明次序被初始化。
static對象初始化問題:
所謂static對象,其壽命從構造出來直到程序結束為止。包括:global對象、定義於namespace作用域內的對象、在class內、函數內、以及在file作用域內被聲明為static的對象。函數內的static對象稱為local static對象,其他static對象稱為non-static對象。程序結束時static對象會被自動銷毀,它們的析構函數會在main()結束時被自動調用。
問題是:如果某編譯單元內的某個non-local static對象的初始化動作使用了令一編譯單元內的某個non-local static對象,它所用到的這個對象可能尚未被初始化。C++對“定義於不同編譯單元內的non-local static對象”的初始化次序並無明確要求。
改進方法:將每個non-local static對象搬到static函數中。C++保證,函數內的local static對象會在“該函數被調用期間”首次遇上該對象之定義式時被初始化。
class FileSystem { ... }; // as before FileSystem& tfs() // this replaces the tfs object; it could be { // static in the FileSystem class static FileSystem fs; // define and initialize a local static object return fs; // return a reference to it } class Directory { ... }; // as before Directory::Directory(params) // as before, except references to tfs are { // now to tfs() ... std::size_t disks = tfs().numDisks(); ... } Directory& tempDir() // this replaces the tempDir object; it { // could be static in the Directory class static Directory td; // define/initialize local static object return td; // return reference to it }
請記住:
(1)為內置類型對象進行手工初始化,因為C++不保證初始化它們。
(2)構造函數最好使用初始化列表,而不要在構造函數體內使用賦值操作。初始化列表中列出的成員變量排列次序與class中聲明次序相同。
(3)為免除“跨編譯單元之初始化次序”問題,請以local static對象替換non-local static對象。
-------------------------------------------------------------------------------------------------------------------