基礎議題
條款1:仔細區別pointers和references
沒有所謂的null reference,但可以有null pointer,這個事實意味着使用reference可能會比使用pointers更有效率(不需要測試其有效性)。因此,讓我做下結論:當你知道你需要指向某個東西,而且絕不會改變其他東西,或是當你實現一個操作符而其語法需求由pointers達成,你就應該選擇references。任何其他時候,請採用pointers。
條款2:最好使用C++轉型操作符
舊式幾乎允許你將任何類型轉換爲任何其他類型,這是十分拙劣的,並且他們難以辨識。
C++導入4個新的轉型操作符(cast operator)
-
static_cast
與C舊式轉型有着相同的意義,也有相同的限制(struct不能轉爲int,或double轉爲pointer),且不能移除表達式的常量性。
-
const_cast
最常見的用途是將某個對象的常量性去除掉
-
dynamic_cast
用來執行繼承體系中的“安全的向下轉型或跨系轉型動作”,轉型失敗時,會以一個null指針(當轉型對象是指針)或一個exception(當轉型對象是reference)表現出來。只能用於繼承體系之中。他無法應用在缺乏虛函數的類型身上,也不能改變類型的常量性。
-
reinterpret_cast
最常用的是轉換“函數指針”類型,但這應該避免使用,除非走投無路。
語法規則的演變:
過去習慣的形式:(type) expression
現在的形式:static_cast(expression)
條款3:絕對不要以多態(polymorphically)方式處理數組
以一個簡單的例子引入:
#include <iostream>
class A {
public:
int a;
};
class B: public A {
public:
int b;
};
int main() {
std::cout << "The size of A is " << sizeof(A) << ","
<< "The size of B is " << sizeof(B);
return 0;
}
運行結果:
The size of A is 4,The size of B is 8
通過運行結果分析,derived classs 通常比其 base classes 有更多的data member,所以 derived classs objects 通常都比其 base class objects 來得大。
現在考慮有個函數,用來打印A數組中的每一個A的內容:
void print_A_array(ostream& s, const A array[], int numElements) {
for (int i = 0; i < numElements; ++i) {
s << array[i];
}
}
當將一個A對象數組傳給此函數,沒問題,但如果將一個B對象數組傳給這個函數,將會發生不可預期的結果。array[i]其實是一個“指針算數表達式”的簡寫;它代表的其實是*(array+i)。array是個指針,每次+1操作,所偏移的字節數等於array所指對象的大小,在這個函數中,指的是A,由以上的推論分析,sizeof(A)不等於sizeof(B),所以會發生錯誤。
以下也會發生類似的錯誤,原因一樣:
void delete_array(ostream& logStream, A array[]) {
delete[] array;
}
當把B對象的數組傳入上面的函數,delete[] array必須產生類似這樣的代碼:
for (int i = the number of elements in the array - 1; i >= 0; --i) {
array[i].A::~A();
}
從而產生一樣的錯誤。
條款4:非必要不提供default constructor
所謂 default constructor 的意思是在沒有任何外來信息的情況將對象初始化。如果class缺乏一個default constructor,當你使用這個class時候便會有某些限制。就以下面的例子慢慢解析:
class EquipmentPiece {
public:
EquipmentPiece(int IDNumber);
...;
}
EquipmentPiece bestPiece[10]; //錯誤!無法調用構造函數。
EquipmentPiece* bestPiece = new EquipmentPiece[10]; //錯誤!同樣無法調用構造函數。
有三個方法可以解決上面的問題,分別是non-heap數組,指針數組,raw memory & placemnet new。
non-heap數組:
int ID1, ..., ID9, ID10;
...
EquipmentPiece bestPiece[10] = {
EquipmentPiece(ID1),
EquipmentPiece(ID2),
EquipmentPiece(ID3),
...
EquipmentPiece(ID10)
};
不幸的是,該方法無法應用在heap數組上。
指針數組:
typedef EquipmentPiece* PEP; // PEP是個指向EquimentPiece的指針
PEP bestPieces[10]; // 很好,不需要調用ctor
PEP* bestPieces = new PEP[10]; // 沒問題
數組中的各指針可用來指向一個個不同的Equipment object
for (int i = 0; i < 10; ++i) {
bestPieces[i] = new EquipmentPiece(IDNumber);
}
此法有兩個缺點。第一,必須記得將此數組所指的所有對象刪除。第二,需要一些空間來放置指針,還需要一些空間用來放置EquipmentPiece objects。
raw memory & placemnet new:
void* raw_memory = operator new[](10*sizeof(EquipmentPiece));
// 讓bestPieces指向此塊內存,使這塊內存
// 被視爲一個EquipmentPiece數組
EquipmentPiece* bestPieces = static_cast<EquipmentPiece*>(raw_memory);
// 利用“placement new”構造這塊
// 內存中的EquipmentPiece objects。
for (int i = 0; i < 10; i++) {
new (&bestPiece[i]) EquipmentPiece(IDNumber);
}
這項技術允許你在“缺乏default constructor”的情況下仍能產生對象數組;但並意味着你可以因此迴避供給constructor自變量。
placement new的缺點:大部分程序員不熟悉它,難以維護,另外在數組對象的生命結束時,以手動方式調用其destructors,最後還得調用operator delete[]的方式釋放raw memory(不能採用一般的數組刪除語法)。
大部分添加default constructors是無意義的,添加會影響classes的效率。如果member functions必須測試字段是否真被初始化了,其調用者便必須爲測試行爲付出時間代價,併爲測試代碼付出空間代價,因爲可執行文件和程序都變大了。萬一測試結果爲否定,對應的處理程序又需要一些空間代價。如果class constructors可以確保對象的所有字段都會被正確地初始化,上述所有成本便都可以免除。如果default constructor無法提供這種保證,那麼最好避免讓default constructor-s出現。雖然這可能會對classes的使用方式帶來某種限制,但同時也帶來一種保證:當你真的是用了這樣的classes,你可以預期它們所產生的對象會被完全地初始化,實現上亦富有效率。