1. 程式人生 > >More Effective C++ - 章節一 : 基礎議題

More Effective C++ - 章節一 : 基礎議題

def pre 基礎 poi 地址 否則 相同 不用 完美世界

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++ - 章節一 : 基礎議題