1. 程式人生 > >標準C++四個型別轉換詳解

標準C++四個型別轉換詳解

C++中的型別轉換分為兩種:

1.      隱式型別轉換(而對於隱式變換,就是標準的轉換,在很多時候,不經意間就發生了,比如int型別和float型別相加時,int型別就會被隱式的轉換位float型別,然後再進行相加運算。);

2.      顯式型別轉換。

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

1.     static_cast

static_cast的轉換格式:static_cast< type-id > ( expression )

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

用於類層次結構中基類和子類之間指標或引用的轉換。

進行上行轉換(把子類的指標或引用轉換成基類表示)是安全的;

進行下行轉換(把基類指標或引用轉換成子類表示)時,由於沒有動態型別檢查,所以是不安全的。

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

把空指標轉換成目標型別的空指標。

把任何型別的表示式轉換成void型別。

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

2.     dynamic_cast

        主要用於執行“安全的向下轉型(safe down casting)”,也就是說,要確定一個物件是否是一個繼承體系中的一個特定型別。

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更安全。在多型型別之間的轉換主要使用dynamic_cast,因為型別提供了執行時資訊。

(1).上行轉換

        比如B繼承自A,B轉換為A,進行上行轉換時,是安全的,如下:
#include <iostream>
using namespace std;
class A
{
	// ......
};
class B : public A
{
	// ......
};
int main()
{
	B *pB = new B;
	A *pA = dynamic_cast<A *>(pB); // Safe and will succeed
}

(2).多重繼承之間的上行轉換:

         C繼承自B,B繼承自A,這種多重繼承的關係;但是,關係很明確,使用dynamic_cast進行轉換時,也是很簡單的:

class A
{
	// ......
};
class B : public A
{
	// ......
};
class C : public B
{
	// ......
};
int main()
{
	C *pC = new C;
	B *pB = dynamic_cast<B *>(pC); // OK
	A *pA = dynamic_cast<A *>(pC); // OK
}

         而上述的轉換,static_cast和dynamic_cast具有同樣的效果。而這種上行轉換,也被稱為隱式轉換;比如我們在定義變數時經常這麼寫:B *pB = new C;這和上面是一個道理的,只是多加了一個dynamic_cast轉換符而已。

(3).轉換成void*

可以將類轉換成void *,例如:

class A
{
public:
	virtual void f(){}
	// ......
};
class B
{
public:
	virtual void f(){}
	// ......
};
int main()
{
	A *pA = new A;
	B *pB = new B;
	void *pV = dynamic_cast<void *>(pA); // pV points to an object of A
	pV = dynamic_cast<void *>(pB); // pV points to an object of B
}

        但是,在類A和類B中必須包含虛擬函式,為什麼呢?因為類中存在虛擬函式,就說明它有想讓基類指標或引用指向派生類物件的情況,此時轉換才有意義;由於執行時型別檢查需要執行時型別資訊,而這個資訊儲存在類的虛擬函式表中,只有定義了虛擬函式的類才有虛擬函式表。

(4).如果expression是type-id的基類,使用dynamic_cast進行轉換時,在執行時就會檢查expression是否真正的指向一個type-id型別的物件,如果是,則能進行正確的轉換,獲得對應的值;否則返回NULL,如果是引用,則在執行時就會丟擲異常;例如:

class B
{
	virtual void f(){};
};
class D : public B
{
	virtual void f(){};
};
void main()
{
	B* pb = new D;   // unclear but ok
	B* pb2 = new B;
	D* pd = dynamic_cast<D*>(pb);   // ok: pb actually points to a D
	D* pd2 = dynamic_cast<D*>(pb2);   // pb2 points to a B not a D, now pd2 is NULL
}

這個就是下行轉換,從基類指標轉換到派生類指標。
對於一些複雜的繼承關係來說,使用dynamic_cast進行轉換是存在一些陷阱的;比如,鑽石結構:

D型別可以安全的轉換成B和C型別,但是D型別要是直接轉換成A型別呢?

class A
{
	virtual void Func() = 0;
};
class B : public A
{
	void Func(){};
};
class C : public A
{
	void Func(){};
};
class D : public B, public C
{
	void Func(){}
};
int main()
{
	D *pD = new D;
	A *pA = dynamic_cast<A *>(pD); // You will get a pA which is NULL
}

        如果進行上面的直接轉,你將會得到一個NULL的pA指標;這是因為,B和C都繼承了A,並且都實現了虛擬函式Func,導致在進行轉換時,無法進行抉擇應該向哪個A進行轉換。正確的做法是:

int main()
{
	D *pD = new D;
	B *pB = dynamic_cast<B *>(pD);
	A *pA = dynamic_cast<A *>(pB);
}

這就是我在實現QueryInterface時,得到IUnknown的指標時,使用的是*ppv = static_cast<IX *>(this);而不是*ppv = static_cast<IUnknown *>(this);

dynamic_cast的討論:

         在探究 dynamic_cast 的設計意圖之前,值得留意的是很多 dynamic_cast 的實現都相當慢。

         例如,至少有一種通用的實現部分地基於對類名字進行字串比較。如果你在一個位於四層深的單繼承體系中的物件上執行 dynamic_cast,在這樣一個實現下的每一個 dynamic_cast 都要付出相當於四次呼叫strcmp 來比較類名字的成本。對於一個更深的或使用了多繼承的繼承體系,付出的代價會更加昂貴。

        對 dynamic_cast 的需要通常發生在這種情況下:你要在一個你確信為派生類的物件上執行派生類的操作,但是你只能通過一個基類的指標或引用來操控這個物件。

有兩個一般的方法可以避免這個問題:

        第一個,使用儲存著直接指向派生類物件的指標的容器,從而消除通過基類介面操控這個物件的需要。當然,這個方法不允許你在同一個容器中儲存所有可能的基類的派生類的指標。為了與不同的視窗型別一起工作,你可能需要多個型別安全(type-safe)的容器。

        一個候選方法可以讓你通過一個基類的介面操控所有可能的 Window 派生類,就是在基類中提供一個讓你做你想做的事情的虛擬函式。例如,儘管只有 SpecialWindows 能 blink,在基類中宣告這個函式,並提供一個什麼都不做的預設實現或許是有意義的:

所以:

(1)避免強制轉型的隨時應用,特別是在效能敏感的程式碼中應用 dynamic_casts,如果一個設計需要強制轉型,設法開發一個沒有強制轉型的侯選方案。

(2)如果必須要強制轉型,設法將它隱藏在一個函式中。客戶可以用呼叫那個函式來代替在他們自己的程式碼中加入強制轉型。

(3)儘量用 C++ 風格的強制轉型替換舊風格的強制轉型。它們更容易被注意到,而且他們做的事情也更加明確。

3.     reinpreter_cast

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

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

該運算子的用法比較多。

4.     const_cast

const_cast的轉換格式:const_cast<type_id> (expression)

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

常量指標被轉化成非常量指標,並且仍然指向原來的物件;常量引用被轉換成非常量引用,並且仍然指向原來的物件;常量物件被轉換成非常量物件。

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

#include <iostream>
using namespace std;
class CA
{
public:
	CA() :m_iA(10){}
	int m_iA;
};
int main()
{
	const CA *pA = new CA;
	// pA->m_iA = 100; // Error
	CA *pB = const_cast<CA *>(pA);
	pB->m_iA = 100;
	// Now the pA and the pB points to the same object
	cout << pA->m_iA << endl;
	cout << pB->m_iA << endl;
	const CA &a = *pA;
	// a.m_iA = 200; // Error
	CA &b = const_cast<CA &>(a);
	b.m_iA = 200;
	// Now the a and the b reference to the same object
	cout << b.m_iA << endl;
	cout << a.m_iA << endl;
}