1. 程式人生 > 其它 >More Effective C++ 基礎議題(條款1-4)總結

More Effective C++ 基礎議題(條款1-4)總結

More Effective C++ 基礎議題(條款1-4)總結

條款1:仔細區別pointers和references

  • 如果有一個變數,其目的是用來指向(代表)另一個物件,但是也有可能它不指向(代表)這個變數,那麼應該使用pointer,因為可將pointer設為null,反之設計不允許變數為null,那麼使用reference
  • 以下這是有害的行為,其結果不可預期(C++對此沒有定義),編譯器可以產生任何可能的輸出
    char *pc = 0;       // 將 pointer 設定為null
    char& rc = *pc;     // 讓 refercence 代表 null pointer 的 解引值
  • 沒有null reference, 使用reference可能比pointers更有效率,在使用reference之前不需要測試其有效性
    void printDouble(const double& rd)
    {
        cout < < rd; // 不需要測試rd,它
    } // 肯定指向一個double值
    //相反,指標則應該總是被測試,防止其為空:
    void printDouble(const double *pd)
    {
        if (pd) // 檢查是否為NULL
        {
            cout < < *pd;
        }
    }
  • pointers可以被重新賦值,指向另一個物件,reference卻總是指向(代表)它最初獲得的哪個物件
  • 實現某些操作符。如operator[],操作符應返回某種“能夠被當作assignment賦值物件”

總結

當你知道你需要指向某個東西,而且絕不會改變指向其他東西,或是當你實現一個操作符而其語法需求無法由pointers達成,你就應該選擇reference。任何其他時候,請採用pointers

條款2:最好使用C++轉型操作符

  • 舊式的C轉型方式,它幾乎允許你將任何型別轉換為任何其他型別,這是十分拙劣的

舊式轉型存在的問題:

  • 例如將pointer-to-const-object
    轉型為一個pointer-to-non-const-object(只改變物件的常量性),和將一個pointer-to-base-class-object轉型為一個pointer-to-derived-class-object(完全改變一個物件的型別),其間有很大的差異。但是傳統的C轉型動作對此並無區分
  • 難以辨識,舊式轉型由一小對小括號加上一個物件名稱(識別符號)組成,而小括號和物件名稱在C++的任何地方都有可能被使用

staic_cast:

  • static_cast基本上擁有與 C 舊式轉型相同的威力與意義,以及相同的限制(如不能將struct轉型為int)。
  • 不能移除表示式的常量性,由const_cast專司其職
  • 其他新式 C++ 轉型操作符適用於更集中(範圍更狹窄)的目的
    (type) expression               //  原先 C 的轉型寫碼形式
    static_cast<type>(expression)   //  使用 C++ 轉型操作符

const_cast:

  • const_cast用來改變表示式的常量性(constness)變易性(volatileness),使用const_cast,便是對人類(編譯器)強調,通過這個轉型操作符,你唯一打算改變的是某物的常量性或變易性。這項意願將由編譯器貫徹執行。如果將const_cast應用於上述以外的用途,那麼轉型動作會被拒絕
#include <iostream>
using namespace std;

class Widget {};
class SpecialWidget : public Widget {};
void update(SpecialWidget* psw);
SpecialWidget sw;                           // sw是個 non-const 物件
const SpecialWidget& csw = sw;              // csw 確實一個代表sw的 reference
                                            // 並視之為一個const物件

update(&csw);                               // 錯誤!不能及那個const SpecialWidget*
                                            // 傳給一個需要SpecialWidget* 的函式

update(const_cast<SpecialWidget*>(&csw));   // 可!&csw的常量性被去除了

update((SpecialWidget*)&csw);               // 可!但較難識別 C 舊式轉型語法
  • const_cast最常見的用途就是將某個物件的常量性去除掉

dynamic_cast:

  • 用來轉型繼承體系重“安全的向下轉型或跨系轉型動作”。也就是說你可以利用dynamic_cast,將“指向base ckass objectspointersreferences”轉型為“指向derived(或sibling base)class objectspointersreferences”,並得知轉型是否成功。如果轉型失敗,會以一個null指標或一個exception(當轉型物件是reference)表現出來:
Widget *pw;

update(dynamic_cast<SpecialWidget*>(pw));           // 很好,傳給update()一個指標,指向pw所指的
                                                    // pw所指的SpecialWidget--如果pw
                                                    // 真的指向這樣的東西;否則傳過去的
                                                    // 將是一個 null 指標
void updateViaRef(SpecialWidegt& rsw);
updateViaRef(dynamic_cast<SpecialWidegt&>(*pw));    // 很好,傳給updateViaRef()的是
                                                    // pw所指的SpecialWidget--如果
                                                    // pw真的指向這樣的東西;否則
                                                    // 丟擲一個exception
  • dynamic_cast只能用來協助你巡航於繼承體系之中。它無法應用在缺乏虛擬函式(請看條款24)的型別身上,也不能改變型別的常量性(constness)
  • 如果不想為一個不涉及繼承機制的型別執行轉型動作,可使用static_cast;要改變常量性(constness),則必須使用const_cast

reinterpret_cast:

  • 最後一個轉型操作符是reinterpret_cast。這個操作符的轉換結果幾乎總是與編譯平臺息息相關。所以reinterpret_cast不具移植性
  • reinterpret_cast的最常用用途是轉換"函式指標"型別。
typedef void (*FuncPtr)();      // FuncPtr是個指標,指向某個函式
                                // 後者無須任何自變數,返回值為voids
FuncPtr funcPtrArray[10];       // funcPtrArray 是個陣列
                                // 內有10個FuncPtrs

假設由於某種原因,希望將以下函式的一個指標放進funcPtrArray中

int doSomething();

如果沒有轉型,不可能辦到,因為doSomething的型別與funcPtrArray所能接受的不同。funcPtrArray內各函式指標所指函式的返回值是void,但doSomething的返回值卻是int

funcPtrArray[0] = &doSomething;                             //錯誤!型別不符
funcPtrArray[0] = reinterpret_cast<FuncPtr>(&doSomething);  //這樣便可通過編譯

某些情況下這樣的轉型可能會導致不正確的結果(如條款31),所以你應該儘量避免將函式指標轉型。

補充:

  • More Effective C++沒有過多的對reinterpret_cast操作符進行解釋,但我覺得應該對它進行更多說明,因為它實在是太強大了,也應該對使用規則做出足夠多的說明
  • reinterpret_cast通過重新解釋底層位模式在型別之間進行轉換。它將expression的二進位制序列解釋成new_type,函式指標可以轉成void*再轉回來。reinterpret_cast很強大,強大到可以隨便轉型。因為他是編譯器面向二進位制的轉型,但安全性需要考慮。當其他轉型操作符能滿足需求時,reinterpret_cast最好別用。
  • 更多瞭解可看cpp reference reinterpret_cast

總結:

在程式中使用新式轉型法,比較容易被解析(不論是對人類還是對工具而言),編譯器也因此得以診斷轉型錯誤(那是舊式轉型法偵測不到的)。這些都是促使我們捨棄C舊式轉型語法的重要因素

條款3:絕對不要以多型(polymorphically)方式處理陣列

假設你有一個class BST及一個繼承自BST的class BalancedBST;

class BST {};
class BalancedBST : public BST {};

現在考慮有個函式,用來列印BSTs陣列中的每一個BST的內容

void printBSTArray(ostream& s, const BST array[], int numElements)
{
    for (int i = 0 ; i < numElements; ++i)
    {
        s << array[i];      // 假設BST objects 有一個
                            // operator<< 可用
    }
}

當你將一個由BST物件組成的陣列傳給此函式,沒問題:

BST BSTArray[10];
printBSTArray(cout, BSTArray, 10);      // 執行良好

然而如果你將一個BalancedBST物件所組成的陣列交給printBSTArray函式,會發生什麼事?

BalancedBST bBSTArray[10];
printBSTArrat(cout, bBSTArray, 10);     // 可以正常執行嗎?
  • 此時就會發生錯誤,因為array[i]代表的時*(array+i),編譯器會認為陣列中的每個元素時BST物件,所以array和array+i之間的距離一定是i*sizeof(BST)
  • 然後當傳入由BalancedBST物件組成的陣列,編譯器會被誤導。它仍假設陣列中每一元素的大小是BST的大小,但其實每一元素的大小是BalancedBST的大小。因此當BalancedBST的大小不等於BST的大小時,會產生未定義的行為
  • 當嘗試通過一個·base class·指標,刪除一個由derived class objects組成的陣列,上述的問題還會再次出現,下面是你可能做出的錯誤嘗試

void deleteArray(ostream& os,BST array[])
{
	os << "Delete array,at address" << 
		static_cast<void*>(array) << 'n';
	delete []array;
}

編譯器看到這樣的句子

delete[] array;

會產生類似這樣的程式碼,問題也就跟之前一樣出現了

for(int i = the number of elements in the array-1; i >= 0; --i)
{
    array[i].BST::~BST();       // 呼叫array[i]的 destructor
}

總結:

  • 多型和指標算術不能混用,陣列物件幾乎總是涉及指標的算術運算,陣列和多型不要混用

條款4:非必要不提供default constructor

後續看過條款43,再回頭來補充

總結:

  • 新增無意義的default constructors,也會影響classes的效率。如果class constructors可以確保物件的所有欄位都會被正確地初始化,為測試行為所付出的時間和空間代價都可以免除。如果default constructors無法提供這種保證,那麼最好避免讓default constructors出現。雖然這可能會對classes的使用方式帶來某種限制,但同時也帶啦一種保證:當你真的使用了這樣的classes,你可以預期它們所產生的物件會被完全地初始化,實現上亦富有效率