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 該運算符把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是兩個不同的對象。 |