類型轉換

 

隱式類型轉換

Boolean Conversions

       譯爲bool轉換。當指針、ints和浮點數被賦給一個bool變量時,非0值會隱式轉換爲true0值會隱式轉換爲false

Integral Conversions

       譯爲整型轉換。當不同類型的ints之間相互賦值時,要遵循如下三條規則:

1.       如果目的數據類型是signed,那麼當源數據超過了目的數據類型的取值範圍,其結果依賴於實現;

2.       如果目的數據類型是unsigned,那麼當源數據超過了目的數據類型的取值範圍,則截取源數據的低位給目的數據;

3.       boolenum可以隱式轉換成能夠容納其最大值的最小位數的ints

Floating Point Conversions

譯爲浮點轉換。是一種當不同類型的浮點數之間相互賦值時進行的轉換——主要是單精度與雙精度間的轉換。轉換的核心是如果源數據在目的數據類型的取值範圍中,則可以轉換;否則轉換的行爲就是未定義的。但是由於浮點數的精度問題,可能找不到與源數據相等的目的數據,那麼與源數據毗鄰的兩個目的數據都有可能被取到,這依賴於具體的實現。

Floating-Integral Conversions

譯爲浮點-整型轉換。這是浮點數與ints數相互賦值時發生的轉換。主要分爲整型轉到浮點型和浮點型轉到整型兩個部分:

1.       浮點轉整型。浮點型的小數部分被捨去。如果浮點數大於目的數據類型的取值範圍時,轉換的行爲是未定義的。

2.       整型轉浮點。增加精度就是了。不過,當一個整型的數據是浮點數無法精確表示的話,會損失精度。例如:

float f = 1234567890;

int i = 1234567890;

f = i;

中,f不等於1234567890,而是等於1234567936

Integral promotion

譯爲整型提升。設計整型提升的目的是爲了讓算術計算中的ints(charshort int)型的操作數佔據一個合適的長度(nature size),從而提高計算的效率。瞭解內存對齊的人都知道,在32位機器上CPU一次從內存讀取的數據是4個字節,這時4個字節(int)就是合適的長度了(nature size)。也就是說CPU直接讀出數據就可以無需經過任何處理的使用,無疑就提高了計算的效率了。從另一個角度來說,如果將charshort int提升到int,在擴大表示範圍的同時,自然也減少了計算時溢出的可能性。

可以看出,一般而言整型提升是在算術計算中使用,我會在接下來的算術轉換中指出整型提升的使用的時機。不過整型提升還會用在可變參數函數的實現中:因爲函數原型的缺乏,函數參數的類型和數目是事先未知的,所有的參數(除了固定的參數)都是按照整型提升的規則存儲在函數所在的frame中——在變參函數中,C++爲了保持與C語言的兼容性,同時會將float提升爲double

現在我們明確了整型提升的設計目的和適用範圍了,接下來討論整型提升的規則:

1.       signedunsignedcharshortshort int在算式中將會被轉換爲int:如果算式中的某個值超過了int的取值範圍,則都會轉換爲unsigned int

2.       在有wchar_t類型和enum類型的算式中,這兩種類型的值會依次測試intunsigned intlongunsigned long四種類型。一旦遇到能容納其最大值的類型就結束測試,轉換成該類型。

3.       位域在算式中的轉換遵循規則1。但如果某位域的值超過了unsigned int的取值範圍,則不會使用整型提升。(這從另一個角度證明,位域並非侷限於一個字節以內)

4.       bool類型的值會轉化爲inttrue轉換爲1false轉換爲0.

從這一節我們瞭解到整型提升是一般算術轉換中的一部分,而且還會在可變參數函數實現中的宏var_arg內使用:進一步的,在var_arg中會額外地將float提升爲double

Arithmetic Conversions

       譯爲算術轉換。以下是摘抄自C++ Primer Plus的原文。模擬了編譯器對一個表達式進行轉換的流程。注意,整個處理主要分爲兩個部分:首先,如果表達式中有浮點數的存在,則將所有數據轉化爲表達式中取值範圍最大的那個浮點數據對應的浮點類型再計算;然後,如果表達式中沒有浮點數,則首先對表達式使用整型提升規則。然後就像處理有浮點數的表達式一樣,將所有數據轉化爲表達式中取值範圍最大的那個數據對應的ints類型,最後再計算;

1. If either operand is type long double, the other operand is converted to long double.

2. Otherwise, if either operand is double, the other operand is converted to double.

3. Otherwise, if either operand is float, the other operand is converted to float.

4. Otherwise, the operands are integer types and the integral promotions are made.

5. In that case, if either operand is unsigned long, the other operand is converted to

unsigned long.

6. Otherwise, if one operand is long int and the other is unsigned int, the conversion

depends on the relative sizes of the two types. If long can represent possible unsigned

int values, unsigned int is converted to long.

7. Otherwise, both operands are converted to unsigned long.

8. Otherwise, if either operand is long, the other is converted to long.

9. Otherwise, if either operand is unsigned int, the other is converted to unsigned int.

10. If the compiler reaches this point in the list, both operands should be int.

小結

       總的來說,類型轉換應該遵循自下轉向上的原則,這樣才能保證精度不損失(注意,intfloat不一定);但凡是自上而下的轉換,一旦源數據大小超過了目標數據類型取值範圍,其行爲是未被定義的——除了bool轉換和整型轉換中目標數據類型是unsigned的轉換。整型提升是一個比較陌生的概念,需要特別注意它在算術轉換中使用的時機和在變參函數實現中使用的位置。


 

顯式類型轉換

C-Style

       這是很熟悉的方式,格式如下:

Type des = (Type)Ori

需要注意的是,(Type)Ori並沒有修改Ori值,而是產生了一個Type類型的新值(這是一個rvalue),並將其賦值給des。而在此過程中,內建數據類型和類的處理是不一樣的:如果Type是內建的數據類型(ints和浮點數),那麼取址操作符&是絕對不能在表達式的括號前使用的——有人用反彙編跟蹤過,發現當Type是內建類型時,新產生的值是在寄存器而非內存中,自然是無法用&取地址的;而如果Type是類,那麼新產生的值會在內存中,所以取址操作符&就可以在表達式的括號前使用,例程如下:

#include <iostream>

using namespace std;

class tTmpObject{

public:

       tTmpObject(){

              cout<<"I'm constructor of father!/n";

       }    

       tTmpObject(const tTmpObject& rn){

              cout<<"I'm Copy con of father!/n";

       }

       ~tTmpObject(){

              cout<<"I'm de of father!/n";

 

       }

       void myprint(){ cout<<"father/n";}

};

class tSub: public tTmpObject{

public:

       tSub(){

              cout<<"I'm constructor of Son!/n";

       };

       tSub(const tSub& rn){

              cout<<"I'm Copy con of son!/n";

       }

       ~tSub(){

              cout<<"I'm de of son!/n";

       }

};

int main()

{    

       tSub s;

       tTmpObject *k ;

       k = &(tTmpObject)s; //新產生了一個tTmpObject對象,調用了tTmpObject的複製構造函數

       tSub *p = &s;

       cout<<&p<<endl<<&k;   //k!=p

    float *fp = &(float)i;  //錯誤

}

程序中,k指向的地址和p指向的地址是不一樣的,也就是說程序通過k = &(tTmpObject)s語句新產生了一個tTmpObject的對象。有點奇怪吧,用指針k通過對象轉換獲取一個地址,結果產生了新的對象,呵呵。

C++-Style

       上面這種C-Style的轉換方式在Bjarne Stroustrup看來是不嚴謹的,因此C++通過引進四個新的類型轉換操作符企圖克服C風格類型轉換的缺點,這四個操作符如下所示:

static_cast:執行上面所提到的隱式轉換罷了,它並不能實現C風格中某些任意不相干類型間的轉換。它是唯一一個可以處理對象(非指針非引用)的轉換操作符。但是如果在進行up-casting的時候,最好用指針轉換。還有一點,static_cast也不能進行暴力轉換,如,將某個類的指針指向另一個不相干的類,要做這樣的事情,就必須用C-style的轉換了。

const_cast:僅僅適用於指向constvolatile對象指針轉換;也就是說,只是可以把一個指向constvolatile的指針的constvolatile特性去除,但是一樣不能修改constvolatile對象的值。我想這個最大的用處就在函數的參數傳遞中吧。

dynamic_cast:這個操作符的作用是使父類的指針(引用)能夠轉化爲子類的指針(引用),也就是常說的向下映射(Downcasting)。不過,由於該操作符檢測匹配性(源指針指向的真實Object必須是目標指針的父類或是自身)用到了RTTI,所以會降低運行效率,在能肯定轉化正確的情況下,可以用static_cast替代。dynamic_cast還有幾個需要注意的地方:第一,該操作符只用於指針和引用;第二,如果不符合轉換條件,對指針賦NULL,對引用則拋出一個bad_cast異常。

reinterpret_cast:其轉換結果幾乎都是執行期定義(implementation-defined)。它也是僅僅適用於指針的。這是一種底層的轉換,它將一切都視爲純粹的內存地址,也就是說可以用一個int指針直接指向一個類。因此,使用reinterpret_casts的代碼很難移植,也很不安全。最普通的用途就是在函數指針類型之間進行轉換。

以上幾個操作符具體在使用中的正確語法爲:

static_cast<type>(expression)

自定義類型轉換

       這是牽涉到非繼承性類之間、類和C內建類型之間的特殊的類型轉換。主要有兩種:構造函數轉換法和conversion operator轉換法。

構造函數轉換法

對於某一特定類MyClass,定義兩個構造函數如下:

MyClass(int i){…}

MyClass(OtherClass o){…}

再定義全局函數如下:

{…}

那麼,可以如下使用MyClass

OtherClass o;

MyClass t1=2;

MyClass t2=o;

func(t1);; func(o);

可以看出,這看上去是完成了從intMyClass,從OtherClassMyClass的轉化,只不過使用的是MyClass不同的構造函數。不過這實際上是一種僞轉換,因爲在函數func中使用的還是MyClass類,而非OtherClass或是int變量。很奇妙,不是麼?需要特別指出的一點就是,如果還定義了一個全局函數爲

{…}

那麼func(2)調用的不是func(MyClass)而是func(int)!這涉及到函數重載時的優先選擇問題,見附錄。

例程如下:

#include <iostream>

using namespace std;

class Otherclass

{

 

};

class MyClass

{

public:

       MyClass(){cout<<"default constructor!/n";}

       MyClass(int i){cout<<"int constructor!/n";}

       MyClass(Otherclass o){cout<<"Otherclass constructor!/n";}

};

void func(MyClass test){}

int main()

{

       MyClass t;

       MyClass t1 = 1;

       Otherclass o;

       MyClass t2 = o;

       func(t);

       func(1);

       func(o);

       return 0;

}

//輸出爲:

//default constructor!

//int constructor!

//Otherclass constructor!

//int constructor!

//Otherclass constructor!

//Press any key to continue . . .

總的來說,如果想使用這種方法,只需要在指定類中定義擁有不同類型參數的構造函數,即可將那些參數類型的對象在需要指定類的情況下轉爲指定類的對象。實際上,在操作符重載的時候就會用到這個機制,例程如下:

#include <iostream>

using namespace std;

 

class tOperator

{

public:

    int i;

    tOperator(int ii):i(ii){}

    const tOperator operator+(const tOperator& cr) {

       cout<<"I'm tOperator's member function!/n";

       return tOperator(this->i+cr.i);

    }

};

 

const tOperator operator+(const tOperator& crl,const tOperator& crr) {

    cout<<"I'm a global function!/n";

    return tOperator(crl.i+crr.i);

}

 

int main()

{

    tOperator to(1);

    tOperator to1(2);

    tOperator to2 = 5+to;  //用到了該特性,將轉化爲tOperator對象

    cout<<to2.i;

}

       不過,在某些情況下,我們並不需要進行這種轉換,但是又需要保留所有的構造函數。這時,我們可以在構造函數的前面加上explicit關鍵字即可禁止這種轉換

conversion operator轉換法

       這個是更加有意思的轉換。如果說構造函數轉換法是一種僞轉換的話,那麼通過conversion operator的轉換就是真正的轉換了。不多說,例程如下(這是一個從《ANSI-ISO C++ Professional Programmer's Handbook》拿來的例程,只是修改了一下)

#include <iostream>

using namespace std;

struct DateRep //legacy C code

{

       char day;

       char month;

       short year;

};

class Date // object-oriented wrapper

{

private:

       DateRep dr;

public:

       operator DateRep () const { return dr;} // automatic conversion to DateRep

       operator int () const {return 1;}

};

void func(DateRep d){cout<<"funcDateRep/n";}

void funci(int i){cout<<"funci/n";}

int main()

{

       Date d;

       func(d);

       funci(d);      

       return 0;

}

//輸出爲

//funcDateRep

//funci

//Press any key to continue . . .

看,Date類可以作爲funcfunci的參數!這都是因爲Date中兩個conversion操作符(大紅色標出)的作用。現在,在funcfunci中使用的就必須是DateRep類和int型了。其實,可以自己實現一個將string類用在strcmp等接受const char*的函數中的封裝類。

       conversion操作符的定義規則如下:

1.       以關鍵字operator開頭;

2.       後面接需要轉換爲的數據類型,事實上這就是返回值——如上面程序中的intDateRep

3.       函數的()中不許有參數;

4.       在函數體中返回前面指定的數據類型。

附錄

重載函數的選擇規則

       這裏暫時不考慮類繼承樹中的問題,假設考慮的重載函數們都位於同一個名詞空間中,那麼選擇的規則如下:

1.       首先編譯器進行精確的匹配。也就是說,不對參數進行任何形式的convert(除了數組名轉爲指針,函數名轉爲指針,Type轉爲const Type),直接尋找能匹配的函數。

2.       使用整型提升進行匹配。嗯,是C風格的整型提升,包括float提升爲doubledouble提升爲long double

3.       使用隱式轉換進行匹配。

4.       使用自定義類型轉換進行匹配。

開始處理可變參數的類型匹配。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章