More Effective C++ - 章節一 : 基礎議題
1. 仔細區分 pointers 和 references
references和pointers的差別描述如下:
pointer:當需要考慮"不指向任何對象"時,或者是考慮"在不同時間指向不同對象"的能力時,應該采用pointer。前一種情況可以將pointer設為null,後一種可以改變pointer所指對象。
reference:當確定"總是會代表某個對象",而且"一旦代表了該對象就不能再改變",那應該使用reference。 reference不能為空,因此無需做判空操作,相對pointer使用效率更高。某些操作符重載 operator[],operator= 采用reference實現。
結論: 當知道需要指向某個東西,而且絕不會改變指向其他東西,或是當實現一個操作符而其語法需求無法由pointers實現,就應選擇references。 其任何時候,請采用 pointers。
2. 最好使用 C++ 轉型操作符
由於舊式的c轉型幾乎允許任何類型轉換為任何其他類型,這樣是十分拙劣的,如果每次轉型能夠精確地指明轉型意圖,會更好。還有就是舊式轉型難以辨識,導致查看代碼時,會遺漏轉型操作。C++ 引進4個新的轉型操作符:
舊式 C 轉型:
(type) expression
C++ 轉型:
1. static_cast<type>(expression) : 基本擁有與 C 舊式轉型相同的威力和意義 2. dynamic_cast<type>(expression) : 用來執行繼承體系中"安全的向下轉型或者跨系轉型動作",我們可以利用dynamic_cast,將 "指向父類對象的指針或引用" 轉型為 "指向子類對象的指針或者引用" ,並得知是否轉型成功,轉型失敗會返回一個null指針(當轉型對象時指針)或一個exception(當轉型對象時reference) 3. const_cast<type>(expression) : 改變表達式常量性(constness)或者 變易性(volatileness) 4. reinterpret_cast<type>(expression) : 與編譯平臺相關,不具有移植性。最常用用途是轉換 "函數指針" 類型,不到非用不可的地步不用,因為某些情況下會出現轉型不正確。 例如: typedef void (*FuncPtr)(); FuncPtr funcPtrArrary[10]; int doSomething(); funcPtrArrary[0] = reinterpret_cast<FuncPtr> (&doSomething); // 將返回值為 int 的函數指針轉換為返回值為void*
如果你想為一個不涉及繼承機制的類型執行轉型動作,可使用static_cast;要改變常量性,必須使用const_cast;涉及繼承機制,使用dynamic_cast 或者 static_cast; reinterpret_cast 把一個指針轉換成一個整數,也可以把一個整數轉換成一個指針。
3. 絕對不要以多態的方式處理數組
大致代碼如下:
// 簡單繼承關系 class BST{...}; class BalancedBST: public BST{...}; // 打印接口 void printfBSTArray(ostream& s, const BST arrry[], int numElements) { for(int i = 0; i < numElements; ++i){ s << array[i]; } } BalancedBST bBSTArray[10]; ... printfBSTArray(cout, bBSTArray, 10); // 發生不可預期問題
上述代碼中,編譯器為了能夠訪問整個數組,編譯器必須有能力決定數組對象大小,編譯器認為大小為BST類大小,但是我們傳入的是其子類,而子類對象大小肯定是大於父類的,因此導致這裏會發生不可預期錯誤。
多態和指針算術不能混用。而數組對象幾乎總是會涉及指針的算術運算,所以數組和多態不要混用。
4. 非必要不提供 default constructor
由不帶參數的構造函數,或者為所有的形參提供默認實參的構造函數,被稱為默認構造函數(default constructor)。
class foo
{
public:
foo(); // 默認構造函數
...
};
在一個完美世界中,凡可以"合理的從無到有生成對象"的class,都應該內含默認構造函數,而"必須有某些外來信息才能生成對象"的class,則不必擁有默認構造函數。但是,當一個類缺乏default constructor,使用時會受到限制。
class foo
{
public:
foo(int Id); // 構造函數
...
};
foo bestfoo[10]; // 錯誤! 無法調用構造函數
foo *best = new foo[10]; // 錯誤! 無法調用默認構造函數
有介紹三種辦法來解決無默認構造函數的限制:
1. non-heap 數組
foo fooTest[] = {foo(1), foo(2), foo(3)}; // 構造函數獲得id,可以成功
2. 指針數組。 缺點:指針數組要刪除,否則內存泄漏;保存指針,浪費內存。
typedef foo* foo_ptr;
foo_ptr bestfoo[10]; // 沒問題,存在的指針,不用調用構造函數
foo_ptr *best = new foo_ptr[10] // 也沒問題,數組堆上存的是指針
for(int i = 0; i < 10; ++i){
best[i] = new foo(Id number); // 傳入ID 初始化對象
}
3. placement new。分配內存時可指定內存位置。
// 首先申請一塊適當大小的緩存,可以是堆上的,也可以是其他特殊緩存(例如共享內存)
void *rawMemory = operator new[](10*sizeof(foo));
// 讓 best 指針指向該塊內存首地址
foo* best = static_cast<foo*>(rawMemory);
// 然後利用 placement new 構造內存中的 foo 對象
for(int i = 0; i < 10; ++i){
new (&best[i]) foo(Id number); // (&best[i]) : 內存位置
}
缺乏 default constructor 類的第二個缺點,將不適用於許多基於模板實現的容器類。
第三個缺點,虛基類沒有默認構造函數,將導致所有繼承類都要註意提供虛基類構造函數自變量,十分麻煩。
缺乏默認構造雖然會導致上述三種限制,但是如果不需要默認構造函數的類卻加了默認構造函數,將導致該類內部member functions邏輯變得復雜,以及影響調用該類的客戶代碼的效率。 因此非必要不提供 default constructor。
2018年10月1日14:59:02
More Effective C++ - 章節一 : 基礎議題