天天看點

C++中的強制類型轉換

C++中的強制類型轉換(轉貼)

這幾天修改系統的bug,每天都會遇到一些很“有趣”的事情,寫出來和大家分享。該系統的壽命也不算短,有五、六年了。開發人員換了多代,問題層出不窮。

沒有自動化測試來保障的系統就是這樣,改正一個bug可能引入更多其它的 bug,錯誤率一直具高不下;開發人員也變的“畏手畏腳”的,不敢對系統進行大膽地重構,代碼結構就越來越亂,要想添加或者修改一個特性也就越來越困難。整個一“惡性循環”,真是苦不堪言。

1. 真是奇怪,到底哪兒出了問題

       上午剛上班,興沖沖地打開測試資料庫,先給未改正的bug排一個優先級。排名第一的是一個嚴重的“指針錯誤”:從視窗上删除一個組合後的圖示會産生指針錯誤,提示通路非法記憶體位址空間。

       根據經驗,修改這類錯誤最有效的方法是進行調試跟蹤。通過調試,終于找到了出錯的代碼。

     …

DelIconInfo(((CIcon*)Icon)->ansIconName);

錯誤是找到了,but why?

偶以為是由于Icon是空指針造成的,可檢視一下,它不是空指針,再看一下ansIconName,它是一個空字元串,但這不會造成非法通路記憶體空間的錯誤啊?汗啊!

2. 柳暗花明

       記得國中的時候看過陳青雲的一本小說,主人公是一位出色的劍客。他的一句話讓我印象特深刻:越是困難和危險的時候越要保持頭腦冷靜。

       仔細檢視了一下代碼,終于發現了問題所在。上面程式中Icon取自一個連結清單,而連結清單添加的時候是CGraphElement類型的。代碼大緻如下:

    //

//往連結清單裡添加元素 位于函數f1

       CgraphElement *pElement;

    ….

pList->Add(pElement);

//對連結清單進行處理 位于函數f2

for(each Icon in pList){

    DelIconInfo(((CIcon*)Icon)->ansIconName);

}

呵呵,添加和處理的指針類型是不一緻的,又采用了“暴力”的強制類型轉換,不出問題才怪那。欣喜之下,偶仔細檢視了一下CgraphElement和Cicon的定義。

class CGraphElement: public Cobject{

         …

};

class CIcon: public Cobject{

     …

         String ansIconName;

};

CgraphElement對象本來沒有ansIconName這個屬性,把它強制類型轉換後,雖然編譯通過,但((CIcon*)Icon)->ansIconName指向的實際上是一個不屬于自己的位址空間,難怪會報“非法通路位址空間”的錯誤了。

3. 亡羊補牢,為時為晚

       去年實習面試時,Boss對偶說:犯錯誤并不可能怕,關鍵要知錯能改。

       其實,關于強制類型轉換的問題,很多書都讨論過,寫的最詳細的是C++ 之父的《C++ 的設計和演化》。最好的解決方法就是不要使用C風格的強制類型轉換,而是使用标準C++的類型轉換符:static_cast, dynamic_cast。标準C++中有四個類型轉換符:static_cast、dynamic_cast、reinterpret_cast、和const_cast。下面對它們一一進行介紹。

3.1 static_cast

         用法:static_cast < type-id > ( expression )      

       該運算符把expression轉換為type-id類型,但沒有運作時類型檢查來保證轉換的安全性。它主要有如下幾種用法:

l         用于類層次結構中基類和子類之間指針或引用的轉換。進行上行轉換(把子類的指針或引用轉換成基類表示)是安全的;進行下行轉換(把基類指針或引用轉換成子類表示)時,由于沒有動态類型檢查,是以是不安全的。

l         用于基本資料類型之間的轉換,如把int轉換成char,把int轉換成enum。這種轉換的安全性也要開發人員來保證。

l         把空指針轉換成目标類型的空指針。

l         把任何類型的表達式轉換成void類型。

注意:static_cast不能轉換掉expression的const、volitale、或者__unaligned屬性。

3.2 dynamic_cast

              用法:dynamic_cast < type-id > ( expression )

       該運算符把expression轉換成type-id類型的對象。Type-id必須是類的指針、類的引用或者void *;如果type-id是類指針類型,那麼expression也必須是一個指針,如果type-id是一個引用,那麼expression也必須是一個引用。

       dynamic_cast主要用于類層次間的上行轉換和下行轉換,還可以用于類之間的交叉轉換。

       在類層次間進行上行轉換時,dynamic_cast和static_cast的效果是一樣的;在進行下行轉換時,dynamic_cast具有類型檢查的功能,比static_cast更安全。

       class B{

public:

       int m_iNum;

       virtual void foo();

};

class D:public B{

    public:

       char *m_szName[100];

};

void func(B *pb){

    D *pd1 = static_cast<D *>(pb);

    D *pd2 = dynamic_cast<D *>(pb);

}

在上面的代碼段中,如果pb指向一個D類型的對象,pd1和pd2是一樣的,并且對這兩個指針執行D類型的任何操作都是安全的;但是,如果pb指向的是一個B類型的對象,那麼pd1将是一個指向該對象的指針,對它進行D類型的操作将是不安全的(如通路m_szName),而pd2将是一個空指針。另外要注意:B要有虛函數,否則會編譯出錯;static_cast則沒有這個限制。這是由于運作時類型檢查需要運作時類型資訊,而這個資訊存儲在類的虛函數表(關于虛函數表的概念,詳細可見<Inside c++ object model>)中,隻有定義了虛函數的類才有虛函數表,沒有定義虛函數的類是沒有虛函數表的。

另外,dynamic_cast還支援交叉轉換(cross cast)。如下代碼所示。

class A{

public:

        int m_iNum;

      virtual void f(){}

};

class B:public A{

};

class D:public A{

};

void foo(){

       B *pb = new B;

    pb->m_iNum = 100;

    D *pd1 = static_cast<D *>(pb);    //copile error

    D *pd2 = dynamic_cast<D *>(pb); //pd2 is NULL

delete pb;

}

在函數foo中,使用static_cast進行轉換是不被允許的,将在編譯時出錯;而使用 dynamic_cast的轉換則是允許的,結果是空指針。

3.3 reinpreter_cast

       用法:reinpreter_cast<type-id> (expression)

type-id必須是一個指針、引用、算術類型、函數指針或者成員指針。它可以把一個指針轉換成一個整數,也可以把一個整數轉換成一個指針(先把一個指針轉換成一個整數,在把該整數轉換成原類型的指針,還可以得到原先的指針值)。

該運算符的用法比較多。

3.4 const_cast

       用法:const_cast<type_id> (expression)

       該運算符用來修改類型的const或volatile屬性。除了const 或volatile修飾之外, type_id和expression的類型是一樣的。

常量指針被轉化成非常量指針,并且仍然指向原來的對象;常量引用被轉換成非常量引用,并且仍然指向原來的對象;常量對象被轉換成非常量對象。

Voiatile和const類試。舉如下一例:

class B{

       public:

        int m_iNum;

}

void foo(){

const B b1;

b1.m_iNum = 100;            //comile error

B b2 = const_cast<B>(b1);

b2. m_iNum = 200;           //fine

    }

    上面的代碼編譯時會報錯,因為b1是一個常量對象,不能對它進行改變;使用const_cast把它轉換成一個常量對象,就可以對它的資料成員任意改變。注意:b1和b2是兩個不同的對象。