More Effective C++ 基礎議題(Basics)

基礎議題

條款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,你可以預期它們所產生的對象會被完全地初始化,實現上亦富有效率。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章