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