書名: 高質(zhì)量程序設(shè)計(jì)指南:C++/C語言作者名: 林銳 韓永泉編著本章字?jǐn)?shù): 2381字更新時(shí)間: 2019-01-09 14:08:59
4.3 類型轉(zhuǎn)換
當(dāng)在C程序中某個(gè)地方所需要的數(shù)據(jù)類型并非你所提供的類型時(shí),通常要進(jìn)行數(shù)據(jù)類型轉(zhuǎn)換,這是C語言強(qiáng)制類型檢查機(jī)制的具體表現(xiàn)。例如,在一個(gè)同時(shí)出現(xiàn)了不同類型的操作數(shù)的復(fù)合算術(shù)表達(dá)式中,一般占用內(nèi)存較少的類型會(huì)隱式地轉(zhuǎn)換為表達(dá)式中占用內(nèi)存最多的操作數(shù)類型。C++也是一樣,甚至提供了比C更嚴(yán)格的靜態(tài)類型檢查系統(tǒng)。
【提示4-5】: 從本質(zhì)上來說,C++/C不會(huì)直接對(duì)兩個(gè)類型不同的操作數(shù)進(jìn)行運(yùn)算,如果操作數(shù)類型不同,編譯器就會(huì)試圖運(yùn)用隱式類型轉(zhuǎn)換規(guī)則或者按照用戶要求進(jìn)行強(qiáng)制類型轉(zhuǎn)換。類型轉(zhuǎn)換并不是改變?cè)瓉淼念愋秃椭担巧闪诵碌呐R時(shí)變?cè)漕愋蜑槟繕?biāo)類型。
4.3.1 隱式轉(zhuǎn)換
所謂隱式轉(zhuǎn)換,就是編譯器在背后幫程序員做的類型轉(zhuǎn)換工作,程序員往往察覺不到。既然是編譯器自動(dòng)進(jìn)行的,那么這種類型轉(zhuǎn)換必須具有足夠的安全性,這是編譯器的責(zé)任。反過來,凡是不安全的類型轉(zhuǎn)換,編譯器都應(yīng)該能夠檢查出來并給出錯(cuò)誤或警告信息,而不會(huì)默默地執(zhí)行。如果程序員確實(shí)想做這樣的轉(zhuǎn)換,那么需要顯式地使用強(qiáng)制類型轉(zhuǎn)換,由此可能造成的安全隱患由程序員負(fù)責(zé)。
這里的安全性主要包括兩個(gè)方面:內(nèi)存單元訪問的安全性和轉(zhuǎn)換結(jié)果的安全性。主要表現(xiàn)為內(nèi)存訪問范圍的擴(kuò)張、內(nèi)存的截?cái)唷⑽矓?shù)的截?cái)唷⒅档母淖兒鸵绯龅取?/p>
下面我們具體分析一下基本數(shù)據(jù)類型之間的轉(zhuǎn)換,以及C++基類和派生類之間的轉(zhuǎn)換中可能出現(xiàn)的安全性問題。
基本數(shù)據(jù)類型之間存在如下的兼容關(guān)系:char is-a int、int is-a long、long is-a float、float is-a double,并且is-a關(guān)系是傳遞的。但是存在于基本數(shù)據(jù)類型之間的is-a關(guān)系不同于C++派生類和基類之間的is-a關(guān)系,因?yàn)橐粋€(gè)高級(jí)基本數(shù)據(jù)類型(占據(jù)內(nèi)存字節(jié)多的數(shù)據(jù)類型)并不是由一個(gè)或多個(gè)低級(jí)基本數(shù)據(jù)類型(占據(jù)內(nèi)存字節(jié)少的數(shù)據(jù)類型)子對(duì)象構(gòu)造而成的,一個(gè)低級(jí)基本數(shù)據(jù)類型也不是派生自多個(gè)高級(jí)基本數(shù)據(jù)類型的。
一個(gè)低級(jí)數(shù)據(jù)類型對(duì)象總是優(yōu)先轉(zhuǎn)換為能夠容納得下它的最大值的、占用內(nèi)存最少的高級(jí)類型對(duì)象。例如,100這個(gè)字面常量(類型為int)如果轉(zhuǎn)換為long型就能滿足編譯器的要求,那就不會(huì)轉(zhuǎn)換為double型。例如,下面的兩個(gè)重載函數(shù):
void f(long l); void f(double d);
如果存在調(diào)用f(100),則必然調(diào)用f(long l)而不是f(double d),除非不存在f(long l)才會(huì)連接到f(double d)。
示例4-5中的轉(zhuǎn)換是安全的,并不需要強(qiáng)制。編譯器首先隱式地將100提升為double(作為它的整數(shù)部分)的一個(gè)臨時(shí)變量,然后才將這個(gè)臨時(shí)變量賦值給d1;同樣,i也會(huì)首先隱式地提升為double(其值作為它的整數(shù)部分)的一個(gè)臨時(shí)變量,然后才賦值給d2。當(dāng)編譯器認(rèn)為這些臨時(shí)變量不再需要時(shí)就會(huì)適時(shí)地把它們銷毀。
示例4-5
double d1 = 100; int i = 100; double d2 = i;
由于派生類和基類之間的is-a關(guān)系,可以直接將派生類對(duì)象轉(zhuǎn)換為基類對(duì)象,這雖然會(huì)發(fā)生內(nèi)存截?cái)啵菬o論從內(nèi)存訪問還是從轉(zhuǎn)換結(jié)果來說都是安全的。這得益于C++的一個(gè)保證:派生類對(duì)象必須保證其基類子對(duì)象的完整性,即其中的基類子對(duì)象的內(nèi)存映像必須和真正的基類對(duì)象的內(nèi)存映像一致,如圖4-3所示。程序見示例4-6。
示例4-6


圖4-3 基類和派生類之間的隱式轉(zhuǎn)換
【提示4-6】: 標(biāo)準(zhǔn)C語言允許任何非void類型指針和void類型指針之間進(jìn)行直接的相互轉(zhuǎn)換。但在C++中,可以把任何類型的指針直接指派給void類型指針,因?yàn)関oid*是一種通用指針,但是不能反過來將void類型指針直接指派給任何非void類型指針,除非進(jìn)行強(qiáng)制轉(zhuǎn)換。因此,在C語言環(huán)境中我們就可以先把一種具體類型的指針,如int*轉(zhuǎn)換為void*類型,然后再把void*類型轉(zhuǎn)換為double*類型,而編譯器不會(huì)認(rèn)為這是錯(cuò)誤的。然而這樣的做法確實(shí)存在著不易察覺的安全問題(內(nèi)存擴(kuò)張和截?cái)嗟龋@是標(biāo)準(zhǔn)C語言的一個(gè)缺陷。
4.3.2 強(qiáng)制轉(zhuǎn)換
我們來看看強(qiáng)制類型轉(zhuǎn)換可能導(dǎo)致的安全問題。首先來看基本數(shù)據(jù)類型及其指針的轉(zhuǎn)換,見示例4-7。
示例4-7
double d3 = 1.25e+20; double d4 = 10.25; int i2 = (int)d3; int i3 = (int)d4;
按照從浮點(diǎn)數(shù)到整型數(shù)的轉(zhuǎn)換語義,結(jié)果應(yīng)該是截去浮點(diǎn)數(shù)的小數(shù)部分而保留其整數(shù)部分,因此i3會(huì)得到10,而i2會(huì)溢出,因?yàn)閐3的整數(shù)部分遠(yuǎn)遠(yuǎn)超出了一個(gè)int所能表示的范圍,結(jié)果當(dāng)然不是我們所期望的。
基本數(shù)據(jù)類型之間的指針轉(zhuǎn)換一般說來必然會(huì)造成內(nèi)存截?cái)嗷騼?nèi)存訪問范圍的擴(kuò)張,除非兩種類型具有相同的字節(jié)大小。例如,在32位系統(tǒng)中,int、long、float都具有4字節(jié)的空間,雖然不會(huì)造成內(nèi)存截?cái)嗷騼?nèi)存擴(kuò)張,但是它們之間的指針轉(zhuǎn)換改變了編譯器對(duì)指針?biāo)赶虻膬?nèi)存單元的解釋方式,因此結(jié)果必然是錯(cuò)誤的,見示例4-8。
示例4-8
double d5 = 1000.25; int *pInt = (int*)&d5; int i4 = 100; double *pDbl = (double*)&i4;
從內(nèi)存訪問角度來說,你通過pInt訪問它指向的double類型變量d5是安全的(后面的4字節(jié)被“截?cái)唷绷耍稍L問內(nèi)存范圍縮小),但是其值絕對(duì)不會(huì)是d5的整數(shù)部分1000,而是位于d5開頭4字節(jié)中的內(nèi)容,并解釋為int類型數(shù),這個(gè)數(shù)是不可預(yù)料的。同樣,你通過pDbl訪問int類型變量i4,得到的數(shù)據(jù)不一定就是100,況且造成了可訪問內(nèi)存范圍的“擴(kuò)張”。如果你往里面寫數(shù)據(jù)就會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤,如圖4-4所示。

圖4-4 基本數(shù)據(jù)類型的指針之間的強(qiáng)制轉(zhuǎn)換
C++基類和派生類之間的指針強(qiáng)制轉(zhuǎn)換同樣存在安全隱患,如示例4-9所示。
示例4-9
Base objB2; Derived *pD2 = (Derived *)&objB2;
存在的問題是:通過pD2能夠訪問的內(nèi)存范圍“擴(kuò)張”了4字節(jié),如果訪問m_c就可能引發(fā)運(yùn)行時(shí)錯(cuò)誤,因?yàn)閜D2指向的對(duì)象根本就沒有成員m_c的空間,如圖4-5所示。

圖4-5 派生類和基類之間的指針強(qiáng)制轉(zhuǎn)換
對(duì)于類層次結(jié)構(gòu)中的轉(zhuǎn)換,C++提供了新的類型轉(zhuǎn)換操作符及能夠起類型轉(zhuǎn)換作用的函數(shù),我們會(huì)在后面講到。
【提示4-7】: (1)不可以把基類對(duì)象直接轉(zhuǎn)換為派生類對(duì)象,無論是直接賦值還是強(qiáng)制轉(zhuǎn)換,因?yàn)檫@不是“自然的”。(2)對(duì)于基本類型的強(qiáng)制轉(zhuǎn)換一定要區(qū)分值的截?cái)嗯c內(nèi)存截?cái)嗟牟煌#?)如果你堅(jiān)持要使用強(qiáng)制轉(zhuǎn)換,必須同時(shí)確保內(nèi)存訪問的安全性和轉(zhuǎn)換結(jié)果的安全性。(4)如果確信需要數(shù)據(jù)類型轉(zhuǎn)換,請(qǐng)盡量使用顯式的(即強(qiáng)制)數(shù)據(jù)類型轉(zhuǎn)換,讓人們知道發(fā)生了什么事,避免讓編譯器靜悄悄地進(jìn)行隱式的數(shù)據(jù)類型轉(zhuǎn)換。
【提示4-8】: 盡量避免做違反編譯器類型安全原則和數(shù)據(jù)保護(hù)原則的事情,例如,在有符號(hào)數(shù)和無符號(hào)數(shù)之間轉(zhuǎn)換,或者把const對(duì)象的地址指派給非const對(duì)象指針,等等。
- Objective-C應(yīng)用開發(fā)全程實(shí)錄
- RabbitMQ Essentials
- NGINX Cookbook
- 響應(yīng)式Web設(shè)計(jì):HTML5和CSS3實(shí)戰(zhàn)(第2版)
- OpenStack Networking Essentials
- Getting Started with Python
- 監(jiān)控的藝術(shù):云原生時(shí)代的監(jiān)控框架
- 深度學(xué)習(xí)程序設(shè)計(jì)實(shí)戰(zhàn)
- Clojure Polymorphism
- Android開發(fā)進(jìn)階實(shí)戰(zhàn):拓展與提升
- 熱處理常見缺陷分析與解決方案
- 軟件測(cè)試項(xiàng)目實(shí)戰(zhàn)之功能測(cè)試篇
- 瘋狂Ajax講義(第3版)
- TensorFlow程序設(shè)計(jì)
- iOS程序員面試筆試真題與解析