1 讓自己習慣 C++
條款01:視 C++ 為一個語言聯邦
將C++視為一個由相關語言組成的聯邦而非單一語言。在某個次語言(sublanguage)中,各種守則與通例都傾向簡單、直觀易懂、并且容易記住。然而當你從一個次語言移往另一個次語言,守則可能改變。
- C:說到底C++仍是以C為基礎。區塊,語句,預處理器,內置數據類型,數組,指針統統來自C。
- Objective-Oriented C++:C with Classes所訴求的。這一部分是面向對象設計之古典守則在C++上的最直接實施。類,封裝,繼承,多態,virtual函數等等...
- Template C++:C++的泛型編程部分
- STL:template程序庫。容器(containers),迭代器(iterators),算法(algorithms)以及函數對象(function objects)...
** note: **
C++高效編程守則視狀況而改變,取決于你使用C++的哪一部分。
條款02:盡量以 const,enum,inline 替換 #define
C++ 編譯過程:預處理 --> 編譯 --> 鏈接
預處理過程掃描源代碼,對其進行初步的轉換,產生新的源代碼提供給編譯器。檢查包含預處理指令的語句和宏定義,并對源代碼進行相應的轉換。預處理過程還會刪除程序中的注釋和多余的空白字符。
“寧可以編譯器替換預處理器”。就是盡量少用預處理。
預處理器
#define ASPECT_RATIO 1.653
將所有出現ASPECT_RATIO的地方替換為1.653,ASPECT_RATIO可能并未進入記號表(symbol table)。因此,當出現錯誤時報的是1.653而不是ASPECT_RATIO,導致目標定位有問題,問題追蹤有困難。如果使用變量,則可輕易地判斷。
此外,盲目地把ASPECT_RATIO替換為1.653可能會在目標碼中出現多份1.653,改用常量絕不會出現相同情況。所以盡量定義為常量,const double ASPECT_RATIO = 1.653
。如果在數組初始化的時候,編譯器需要知道數組的大小,且編譯器(錯誤地)不允許使用“static整數型class常量”進行數組初始化,這時可以使用枚舉類型enum來替代define。
class GamePlays{
private:
static const int NumTurns = 5; // static整數型class常量
enum { NumTurns = 5 }; // 枚舉
int scores[NumTurns];
... ...
}
- 宏看起來像函數,但不會招致函數調用帶來的額外開銷。如果你想獲得高效,建議使用inline內聯函數。
有了consts 、enums 和inlines,我們對預處理器(特別是#define) 的需求降低了,但并非完全消除。#include 仍然是必需品,而 #ifdef / #ifndef 也繼續扮演控制編譯的重要角色。目前還不到預處理器全面引退的時候,但我們要盡量限制預處理器的使用。
** note: **
- 對于單純常量,最好以const對象或enum替換#define。
- 對于形似函數的宏,最好改用inline函數替換#define。
條款03:盡可能使用 const
const允許你告訴編譯器和其他程序員某值應保持不變,只要“某值”確實是不該被改變的,那就該確實說出來。如果const修飾變量,則表示這個變量不可變;如果const修飾指針,表示指針指向的位置不可改變。
- 和指針有關的const判斷:
- 如果關鍵字const出現在星號左邊,表示被指物事常量。
const char *p
和char const *p
兩種寫法意義一樣,都說明所致對象為常量。 - 如果關鍵字const出現在星號右邊,表示指針自身是常量。
const char * p = "hello"; // *p的hello不可變, 與char const * p = "hello"等價
char * const p = "hello"; // 表示p的值不可變,即p不能指向其它位置
- STL迭代器的const:
- 聲明迭代器為const就像聲明指針為const一樣(即聲明一個T* const指針),表示這個迭代器不得指向不同的東西,但它所指的東西的值可以改變。
- 如果想要迭代器所指的東西不可改變(即模擬一個const T*指針),使用const_iterator。
std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begin(); //類似T* const
*iter = 10; //沒問題,改變iter所指物
++iter; //錯誤!iter是const
std::vector<int>const_iterator cIter = vec.begin(); //類似const T*
*iter = 10; //錯誤,*iter是const
iter++; //沒問題,可以改變iter
- 令函數返回一個常量值,可以避免意外錯誤。
如下代碼,錯把==寫成=,一般程序對*號之后進行賦值會報錯,但在自定義操作符面前不會(因為自定義*號后返回的是Rational對象實例的引用,可以拿來賦值,不會報錯)。如果*不寫成const,則下面的程序完全可以通過,但寫成const之后,再對const進行賦值就出現問題了。函數的參數,如果無需改變其值,盡量使用const,這樣可以避免函數中錯誤地將==等于符號誤寫為=賦值符號,而無法察覺。
class Rational { ... };
const Rational operator* (const Rational& lhs, const Rational& rhs);
...
Rational a, b, c;
if(a * b = c)... //把==錯寫成=,比較變成了賦值
- const作用于成員函數,有兩個作用:
- 可以知道哪些函數可以改變對象內容,哪些函數不可以。
- 改善C++效率,通過pass by reference_to_const(const對象的引用)方式傳遞對象可改善C++效率。
下面是常量函數與非常量函數的形式:
class TextBlock{
public:
...
const char& operator[] (std:size_t position) const
{ return text[position]; }
char& operator[] (std:size_t position)
{ return text[position]; }
private:
std::string text;
};
/* 使用operator[] */
TextBlock tb("hello"); //non-const 對象
cout<<tb[0]<<endl; //調用的是non-const TextBlock::operator[]
tb[0] = 'x'; //沒問題,寫一個non-const對象
const TextBlock cTb("hello"); //const 對象
cout<<cTb[0]<<endl; //調用的是const TextBlock:operator[]
cTb[0] = 'x'; //錯誤,寫一個const對象
在C++中,只有被聲明為const的成員函數才能被一個const類對象調用。
- 成員函數是const意味著什么?
- bitwise const主張const成員函數不可以改變對象內任何non-static成員變量。但一個更改了“指針所指物”的成員函數雖然不能算const,但如果只有指針(而非其所指物)隸屬于對象,那么稱此函數為bitwise const不會引發編譯器異議。
- logical const主張成員函數可以修改它所處理的對象內的某些bits,但要在客戶端偵測不出的情況下才得如此。
編譯器默認執行bitwise。如果想要在const函數中修改non-static變量,需將變量聲明為mutable(可變的)。
class TextBlock{
private:
char* pText;
mutable std::size_t textLength; // 即使在const成員函數內,
mutable bool lengthIsValid; // 這些成員變量也可能會被更改。
public:
...
std::size_t length() const;
};
std::size_t TextBlock::length() const{
if (!lengthIsValid){
textLength = std::strlen(pText); //加上mutable修飾后,便可以修改其值
lengthIsValid = true;
}
}
- 避免const和non-const成員函數重復
如果const和non-const成員函數功能相當、代碼重復,編譯時間、維護等會是一個大問題,這時就用non-const函數去調用const函數,但不能反過來。這是因為non-const函數可能會改變對象,const函數承諾不改變對象,const調用non-const就不安全了
class TextBlock{
public:
const char& operator[](std:size_t position) const
...
return text[position];
}
char& operator[] (std:size_t position){
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
};
上面代碼進行了兩次轉型:
- 第一次用static_cast來為*this添加const,這使接下來調用operator[ ]時得以調用const版本;
- 第二次則是用const_cast從const operator[]的返回值轉除const,以符合non-const返回值類型。
** note: **
- 將某些東西聲明為const可幫助編譯器偵測出錯誤用法。const可被施加于任何作用域內的對象、函數參數、函數返回類型、成員函數本體。
- 編譯器強制實施bitwise constness,但你編寫程序時應該使用“概念上的常量性”;
- 當cosnt和non-const成員函數有著實質等價的實現時,令non-const版本調用const版本可避免代碼重復。
條款04:確定對象被使用前已先被初始化
對內置類型(基本類型)手動進行初始化。
內置類型以外的類型,初始化要靠構造函數,要確保每一個構造函數都將對象的每一個成員初始化。
類的構造函數使用成員初值列(member initialization list),而不是在構造函數中進行賦值操作,這樣通常效率更高。因為賦值的版本其實是先進行初始化再進行賦值,而成員初值列版本是直接進行初始化,這對于非內置類型(std::string等)來說顯然后者效率更高。對于內置類型,其初始化和賦值的成本相同,但為了一致性最好也通過成員初值列來初始化。對于const和reference類型必須是初始化,賦值操作是不允許的。base classes更早于derived classes被初始化,class的初值列成員變量的排列順序與其聲明順序相同。
“不同編譯單元內定義之non-local-static對象”的初始化次序。
static對象,其壽命從被構造出來直到程序結束為止,包括global對象,定義于namespace作用域內的對象,在classes內、在函數內、以及在file作用域內被聲明為static的對象。函數內的static對象被稱為local static對象(因為它們對函數而言是local),其他static對象稱為non-local static對象。
// FileSystem源文件 class FileSystem{ public: ... std::size_t numDisks() const; };
extern FileSystem tfs;
// Directory源文件,與FileSystem處于不同的編譯單元
class Directory{
public:
Directory(params);
...
};
Directory::Directory(params){
...
//調用未初始化的tfs會出現錯誤
std::size_t disks = tfs.numDisks();
}
C++對“定義于不同編譯單元內的non-local static對象”的初始化相對次序并無明確定義,因此,為防止A的初始化需要B,但B尚未初始化的錯誤,將每個non-local static對象搬到自己的專屬函數內(該對象在此函數內被聲明為static),然后用戶調用這些函數,而不直接涉及這些對象。
class FileSystem { ... };
FileSystem& tfs(){
static FileSystem fs;
return fs;
}
class Directory { ... };
Directory::Directory(params){
std::size_t disks = tfs().numberDisks();
}
Directory& tempDir(){
static Directory td;
return td;
}
經過上面的處理,將non-local轉換了local對象,這樣做的原理是:函數內的local static 對象會在"該函數被調用期間","首次遇上該對象之定義式"時被初始化,這樣就保證了對象被初始化。使用函數返回的“指向static對象”的reference,而不再使用static對象本身。這樣做的好處是不調用函數時,不會產生對象的構造和析構。但對多線程這樣的方法會有問題。
** note: **
- 為內置對象進行手工初始化,因為C++不保證初始化它們;
- 構造函數最好使用成員初始化列表,而不要在構造函數本體內使用賦值操作。初始化列表列出的成員變量,其排列次序應該和它們在類中的聲明次序相同;
- 為免除“跨編譯單元之初始化次序”問題,請以local static對象替換non-local static對象。