C++中的型別轉換函式
時間:2014.03.10
地點:基地二樓
------------------------------------------------------------------------------------
一、簡述
C++允許編譯器在不同型別之間執行隱式轉換,和C一樣,char可默默轉換為int,short可默默轉換為double,正因為這種隱式轉換,你可以將short型別交給一個期望獲得double型別的函式來處理,這和傳統的C風格一脈相承。但可怕的是C++還可將int轉換為short,以及將double轉換為char等,這可能導致資訊丟失。而在我們自己設計類的時候,也可以選擇提供適當的函式,供編譯器拿來做隱式轉換。
------------------------------------------------------------------------------------
二、隱式轉換方式
1.單變數建構函式:如果建構函式聲明瞭單一引數,或者有多個引數但除第一個引數不做要求外都有預設值可執行隱式轉換:
class Name{
public:
Name(const string& s); //可把string轉換為Name型別
.....
};
class Rational{ public: Rational(int numerator0,int denominator=1); ... };
2.隱式型別轉換操作符:類中擁有一個成員函式,該函式為關鍵詞operator後加一個型別名稱。不能為該函式指定返回型別,因為返回型別已經表現在函式名稱上。例如為讓Ration物件能夠隱式為double,,可以如下定義Rational類:
class Rational{
public:
......
operator double() const; //將Rational轉換為double
};
這樣這個函式就會在下面場景應用中被自動呼叫發生型別轉換
Rational r(1,2); //r的值是1/2
double d=0.5*r; //現在r自動轉換為double然後執行乘法運算
------------------------------------------------------------------------------------
三、問題
提供這類隱式轉換是存在風險的,我們最好不要提供任何型別轉換函式,因為在你不打算也未預期的情況下,他們可能會被呼叫,導致結果不正確而行為又不直觀,程式變得難以除錯。比如:
假設你的類想表現出一個分數,希望像內建型別一樣輸出分數物件內容,即
Rational r(1,2);
cout<<r; //會打印出 1/2
如果你忘了為Rarional 寫operator<<,按你的思路應該會執行列印不成功,因為沒有合適的operator<<可以呼叫。可是,你的編譯器面對上述情況發現不存在operator<<可接受Rational物件時它會想盡辦法執行型別轉換動作,換在本例中它會呼叫operator double順利將Rational隱式轉換為double,成功得到呼叫列印,於是Rational打印出來就是浮點數了而非你想要的分數型別,而不會給出任何提示。當然這裡並不會給你的程式造成災難,但足以顯示隱式型別轉換的缺點,它們可能導致不可預期的函式呼叫。
------------------------------------------------------------------------------------
四、解決辦法
然而,許多時候,這種型別轉換又是必須的,我們的解決辦法是以功能對等的另一種函式去取代型別轉換操作符,比如這裡為了讓Rational轉換為double,我們不用operator doube() const 成員函式,而是改用ToDouble的成員函式去執行型別轉換。
class Rational{
public:
....
double ToDouble() const; //將Rational轉換為double
};
這樣這種型別轉換需顯示呼叫
Rational r(1,2);
cout<<r; //錯誤,Rational沒有operator<<
cout<<r.ToDouble(); //正確
如此,型別就不會再默默呼叫型別轉換函式,一個典型的應用就是我們的string型別,為了從string物件轉換為傳統的C風格字串,提供的辦法也是一個顯示的c_str成員函式來執行轉換行為。現在來考慮但變數建構函式造成的隱式轉換,考慮一個針對陣列結構的模板類,該陣列結構允許使用者指定索引上下限:
template<typename T>
class Array{
public:
Array(int lowBound,int highBound);
Array(int size);
T& operator[](int index);
...
};
上面第一個建構函式允許客戶指定索引範圍,是一個雙變數無預設值建構函式,不會成為型別轉換函式。第二個建構函式允許使用者指定陣列元素個數,可被用來作為一個型別轉換函式,導致有風險的結果。如下,考慮一個對Array<int>物件進行比較動作的函式。
bool operator==(const Array<int>& lhs,
const Array<int>& rhs);
Array<int> a(10);
Array<int> b(10);
...
for(int i=0;i<10;++i)
if(a==b[i]){ //本應該是a[i]=b[i
//do something for when
}
else{
// do something for other
}
上面程式碼試圖將a的每一個元素拿來和b對應的元素做比較,當鍵入a時意外漏下小標,當然希望編譯器能發現這種錯誤,可它現在卻無動於衷。因為它看到operator==函式呼叫時,這裡實參一個為Array<int> 的變數a和一個為int的變數b[i],雖然沒有這樣的operator==可以被呼叫,但編譯器注意到可以呼叫Array<int>建構函式就可以將int轉換為Array<int>物件,於是它放手執行這種轉換,產生類似這樣的程式碼
for(int i=0;i<10;++i)
if(a==static_cast<Array<int>>(b[i]))...
於是迴圈的每一次迭代都是拿a的每次迭代內容來和一個大小為b[i]的臨時陣列(其內容未定義)來做比較。這顯然不是你想要的,但編譯器從不給出任何提示。這樣的程式很難除錯。然而單變數建構函式很多情景下又是必須的,我們不得不提供,那麼如何消除這種隱式轉換的隱患呢?
關鍵詞explicit就是為這個問題而生的。用法簡單易懂。只要將建構函式宣告為explicit即可,這樣編譯器就不能因隱式型別轉換而去呼叫它們。當然顯式轉換是允許的。
template<typename T>
class Array{
public:
...
explicit Aarray(int size); //注意explicit的使用
...
};
Array<int> a(10); //正確
Array<int? b(10); //正確,均是顯式呼叫
if(a==b[i])... //錯誤,無法將int隱式轉換為Array<int>
if(a==Array<int>(b[i]))...// 正確,顯式行為,但這裡無意義
if(a==static_cast< Array<int> >(b[i]))...//正確,顯式行為,但這裡無意義
if(a==(Array<int>)b[i])... //C式風格的型別轉換,正確,但這裡無意義
};
------------------------------------------------------------------------------------
五、總結
總之,型別轉換函式分為兩種,一種是單變數建構函式,它使得其它型別可能偷偷轉換為本型別使用,我的的解決辦法是讓該單變數建構函式explicit一下。還一種形式是轉換型別操作符,它使得本型別可能偷偷轉換為其它型別使用,解決的辦法是我們在類中提供功能相當的成員函式供顯式呼叫,這樣雖有些不便,但消除了風險。總之,對於型別轉換函式我們要保持警覺。