條款27:儘量少做轉型動作(Effective C++)

博客搬家,原地址:https://langzi989.github.io/2016/12/21/條款27-儘量少做轉型動作/

C++規則設計的目標之一是,保證類型錯誤決不可能發生。理論上如果你的程序很乾淨的通過編譯,就表示它並不企圖在任何對象身上執行任何不安全,無意義,愚蠢荒謬的操作。這是一個及其具有價值的保證,不要輕易放棄它。
但是在很多種情況下,我們不得不進行轉型操作,轉型操作破壞了類型系統。這可能會導致任何可能種類的麻煩,有些容易辨識,但是有些可能會很隱晦。所以在需要進行轉型操作的時候一定要慎重,儘量通過設計避免不必要的轉型操作。

類型轉換的形式

首先我們回顧一下類型轉換的語法,因爲通常有三種不同的形式,可寫出相同的類型轉換動作。

  • C風格類型轉換: (T)expression //將expression轉換爲類型T
  • 函數式風格類型轉換: T(expression) //同上
    上面的兩種形式並無差別,純粹只是把小括號擺放的位置不同而已,我們稱上述兩種轉爲爲"舊式轉型"(old style cast)。

C++還提供四中新式轉型(new style):

  • const_cast< T >(expression)

  • dynamic_cast< T >(expression)

  • reinterpret_cast< T >(expression)

  • static_cast< T >(expression)

  • const_cast通常被用來將對象的常量性移除(cast away the constness)。它也是唯一有此能力的C+±style轉型操作符。

  • dynamic_cast主要用來執行類型向下轉型(safe downcasting),也就是用來決定某對象是否歸屬繼承體系中的某個類型。它是唯一無法由舊式語法執行的動作,也是唯一一個可能耗費重大運行成本的轉型動作

  • reinterpret_cast 意圖執行低級轉型,實際動作及結果可能取決於編譯器。這也就表示它不可移植,例如將一個pointer to int 轉型爲int。這一類型轉換在低級代碼以外很少見。

  • static_cast 用來強迫隱式轉換(implicit conversion),例如將non-const對象轉換爲const對象,或者將int轉換爲double等等。也可以用來執行上述多種轉換的反向轉換,例如將void指針轉換爲type指針,將pointer to derive 轉化爲point to bas。但是無法將const轉換爲non-const。

dynamic_cast與static_cast詳解

static_cast是用來強迫隱式類型轉換,它可以用於1.基本數據類型以及指針之間的轉換;2.類層次中基類與子類成員函數指針的轉換;3.類層次結構中基類與子類指針或者引用之間的轉換。
dynamic_cast可以用於1.繼承關係中類指針或者引用之間的轉換;2.包含虛函數之間對象指針的轉換3.以及保證轉換的安全性。

static_cast

用於基本數據類型轉換和指針之間的轉換

char a;
int b = static_cast<int>(a);
char c = static_cast<char>(b);
char *pa = NULL;
int *pb = (int*)pa;
pb = static_cast<int*>(pa); //編譯錯誤static_cast只能用於void指針和type指針之間的轉換
void *pv = static_cast<void*>(pa); //正確
pb = static_cast<int*>(pv);

類層次中基類與子類成員函數指針的轉換

class base {
public:
  base(int t_data) : m_data(t_data) {}
  void printData() { std::cout << m_data << std::endl; }

private:
  int m_data;
};

class child : public base {
public:
  child(int t_data) : base(t_data) {}
  void printData() { std::cout << "this is in the child" << std::endl; }
};
typedef void (base::*basefun)();

int main() {
  base a(10);
  basefun func = &base::printData;
  func = static_cast<basefun>(&child::printData);
  (a.*func)();  //this is in the child
}

類層次結構中基類與子類指針或者引用之間的轉換

上行轉換:子類指針或引用轉換爲基類的指針或引用 —安全
下行轉換:基類的指針或者引用轉換爲子類的指針或引用 —危險(避免這樣做)

class A
{
};
class B:public A
{
};
class C:public A
{
};
class D
{
};

A objA;
B objB;
A* pObjA = new A();
B* pObjB = new B();
C* pObjC = new C();
D* pObjD = new D();

objA = static_cast<A&>(objB);     //轉換爲基類引用    
objA = static_cast<A>(objB);
objB = static_cast<B>(objA);      //error 不能進行轉換  

pObjA = pObjB;                    //right 基類指針指向子類對象
//objB = objA;                      //error 子類指針指向基類對象
pObjA = static_cast<A*>(pObjB);   //right 基類指針指向子類
pObjB = static_cast<B*>(pObjA);   //強制轉換 OK 基類到子類
//pObjC = static_cast<C*>(pObjB);   //error 繼承於統一類的派生指針之間轉換
//pObjD = static_cast<D*>(pObjC);   //error 兩個無關聯之間轉換

dynamic_cast

繼承關係的類指針對象或者引用之間的轉換

若積累中沒有虛函數,使用dynamic_cast可以將子類的指針或引用轉換爲基類的指針或引用,與static_cast用法相同,不同的是,這個時候使用dynamic_cast將基類指針轉換爲子類指針的時候會出現編譯錯誤(static_cast不會,但是很危險)。

class A
{
};
class B:public A
{
};
class C:public A
{
};
class D
{
};

A objA;
B objB;
A* pObjA = new A();
B* pObjB = new B();
C* pObjC = new C();
D* pObjD = new D();
//objA = dynamic_cast<A>(objB);         //error 非引用

objA = dynamic_cast<A&>(objB);
//objB = dynamic_cast<B&>(objA);      //error A 不是多態類型不能轉換 若有虛函數則可以進行轉換

pObjA = dynamic_cast<A*>(pObjB);
//pObjB = dynamic_cast<B*>(pObjA);    //error A 繼承關係 不是多態類型不能轉換
//pObjB = dynamic_cast<B*>(pObjC);    //error C 兄弟關係 不是多態類型不能轉換
//pObjB = dynamic_cast<B*>(pObjD);    //error D 沒有關係 不是多態類型不能轉換

包含有虛函數之間的對象指針的轉換

使用dynamic_cast將基類指針轉換爲子類指針的時候並不是永遠有效:只有基類指針本身指向的就是一個派生類對象的時候有效。其他時候結果爲NULL;

class A
{
Public:
     Virtual ~A(){}
};
class B:public A
{
};
class C:public A
{
};
class D
{
Public:
Virtual ~D(){}
};
pObjB = dynamic_cast<B*>(pObjA);    // worning 繼承關係 父類具有虛函數 多態
pObjB = dynamic_cast<B*>(pObjD);    //worning 沒有關係 D是多態類型可以轉換
//以上結果:pObjB == NULL 此處會發生一個運行時錯誤

dynamic_cast轉換的安全性

當涉及到基類和派生類對象之間的轉換的時候,總使用dynamic_cast會避免很多錯誤,它是安全的,但是它會給程序運行帶來巨大的開銷。
當子類指針轉換爲基類指針的時候,兩種轉型都OK,dynamic_cast開銷較大。
當基類指針轉換爲派生類指針的時候,若基類中沒有虛函數,static_cast不會報錯,但是做法很危險,dynamic_cast編譯不通過。當含有虛函數的時候,若基類指針沒有指向派生類,這個時候會返回NULL,所以也是安全的。

虛函數對於dynamic_cast轉換的作用

爲什麼dynamic_cast轉換類指針的時候需要虛函數呢?
dynamic_cast轉換是在運行時進行轉換,運行時轉換就需要知道類對象的信息(繼承關係等)。
在運行時或者這個信息的是虛函數表指針,通過這個指針可以獲取到該類對象的所有的虛函數,包括父類的。因爲派生類會繼承基類的虛函數表,所以通過這個虛函數表,我們就可以知道類對象的父類,在轉換的時候就可以用來判斷對象有無繼承關係。
所以虛函數對於正確的基類指針轉換爲子類指針是非常重要的。

effective的三點建議

  • 如果可以,儘量避免轉型,特別是在注重效率的代碼中避免dynamic_cast。如果有個設計需要轉型動作,試着發展無需轉型的替代設計。
  • 如果轉型是必要的,試着將它隱藏與某個函數的背後。客戶隨後可以調用該函數,而不需要將轉型放到他們自己的代碼中。
  • 寧可使用C+±style轉型,不要使用舊式轉型。前者很容易辨識出來,而且也比較有着分門別類的職掌
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章