轉載地址:http://hi.baidu.com/thewillreigns/blog/item/67e665c4296e69c038db492d.html
兩者都作為字符用的話是沒有區別的,但當整數用時有區別:
char 整數范圍為-128到127( 0x80__0x7F),
而unsigned char 整數范圍為0到255( 0__0xFF )
多數情況下,char ,signed char 、unsigned char 類型的數據具有相同的特性然而當你把一個單字節的數賦給一個大整型 數域時,便會看到它們在符號擴展上的差異。另一個區別表現在當把一個介於128和255之間的數賦給signed char 變量時編譯器必須先進行數值轉化,同樣還會出現警告。若使用十六進制進行賦值使用unsigned char 要方便一些.根據編譯器具體實現情況不同,char要么和signed char等同,要么和unsigned char等同.
為了代碼移植,一定不要用 char !!! 要使用 int8_t 或 uint8_t !!!
arm-linux-gcc 規定 char 為 unsigned char !!!
用十六進制(0x開頭的)表示的常數,都代表無符號數。
char short 在使用時編譯器會 Promotion 為 int or unsigned int 。
我以前總以為 char 代表有符號,而 unsigned char 代表無符號。現在才知道如果換做 short 或 int , 這樣理解都沒問題,因為c標准明確定義了。但是,唯獨 char 不能這樣理解,因為c標准中對此是 Impementation Defined,就是未明確定義,由具體的編譯器明確定義。
vc編譯器、x86上的 gcc 都把 char 定義為 signed char;而 arm-linux-gcc 卻把 char 定義為 unsigned char 。這樣一來,在代碼移植上就會出現問題。舉個最簡單的例子:
char a = 0xb6; if ( a == 0xb6) puts("hello world !");
在vc 或 x86的gcc 上,都是不會打印出 hello world! 的。用 arm-linux-gcc 編譯,在arm板上,是可以打印出hello world ! 的。是不是很神奇啊??? 我們再變化一下:
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if ( a == 0xb6) puts("a");
if ( b == 0xb600) puts("b");
if ( c == 0xb6000000) puts("c");
在vc 或 x86的gcc 上,只會打印出 c 。用 arm-linux-gcc 編譯,在arm板上,是可以打印出 a 和 c 。是不是發現了什么了呢?
首先,介紹 Integer Promotion 。通俗點說,c在處理整型(char short int)時,都會自動提升為int(如果int范圍不夠,則提升成 unsigned int)。比如 “a == 0xb6”,首先0xb6會當一個int來處理,變為0x000000b6(關於常量,后面還會仔細說明)。a 會提升為int ,假如 char 被定義為有符合的,那么 a 為負數,因為最高位為1,所以 a會提升為 0xffffffb6。假如 char 被定義為無符號的,那么a會提升為 0x000000b6 。
即,在vc 或 x86的gcc 上,a == 0xb6 會變為 0xffffffb6 == 0x000000b6 ,而在 arm-linux-gcc 上,變為 0x000000b6 == 0x000000b6。
對於 short,因為c標准明確規定 不加關鍵字,就代表有符號數。所以,無論在什么編譯器上 b == 0xb600 都會變成 0xffffb600 == 0x0000b600 。
對於 int,本身是int,也就不用 Integer Promotion 了,所以 c == 0xb60000 中 ,c不做任何處理,直接從內存中讀出來,即 0xb60000 == 0xb60000。
最后,簡單說一下常量。我后面會轉帖一部分資料,有興趣可以看看,我現在只說 用八進制(0開頭)或十六進制(0x開頭)表示的常量,他們都會當成無符號數處理! 另外 像 char a = 0xb6; 這句就有兩個 Implementation Defined,一個是char帶不帶符號,另外一個是,假如char為有符合, 0xb6 會當int 0x000000b6 處理,把這個int 變為 有符合的 char 有溢出,會有問題,0xb6本為正數,賦值到a中卻變為負數,具體要怎么處理,c對此也是 Implementation Defined。
以下為轉帖: http://learn.akae.cn/media/ch15s01.html
我們知道,在C語言中char
型占一個字節的存儲空間,一個字節通常是8個bit。如果這8個bit按無符號整數來解釋,取值范圍是0~255,如果按有符號整數來解釋,采用2's Complement表示法,取值范圍是-128~127。C語言規定了signed
和unsigned
兩個關鍵字,unsigned char
型表示無符號數,signed char
型表示有符號數。
那么以前我們常用的不帶signed
或unsigned
關鍵字的char
型是無符號數還是有符號數呢?C標准規定這是Implementation Defined,編譯器可以定義char
型是無符號的,也可以定義char
型是有符號的,在該編譯器所對應的體系結構上哪種實現效率高就可以采用哪種實現,x86平台的gcc
定義char
型是有符號的。這也是C標准的Rationale之一:優先考慮效率,而可移植性尚在其次。這就要求程序員非常清楚這些規則,如果你要寫可移植的代碼,就必須清楚哪些寫法是不可移植的,應該避免使用。另一方面,寫不可移植的代碼有時候也是必要的,比如Linux內核代碼使用了很多只有gcc
支 持的語法特性以得到最佳的執行效率,在寫這些代碼的時候就沒打算用別的編譯器編譯,也就沒考慮可移植性的問題。如果要寫不可移植的代碼,你也必須清楚代碼 中的哪些部分是不可移植的,以及為什么要這樣寫,如果不是為了效率,一般來說就沒有理由故意寫不可移植的代碼。從現在開始,我們會接觸到很多 Implementation Defined的特性,C語言與平台和編譯器是密不可分的,離開了具體的平台和編譯器討論C語言,就只能討論到本書第一部分的程度了。注意,ASCII碼 的取值范圍是0~127,所以不管char
型是有符號的還是無符號的,存一個ASCII碼都沒有問題,一般來說,如果用char
型存ASCII碼字符,就不必明確寫是signed
還是unsigned
,如果用char
型表示8位的整數,為了可移植性就必須寫明是signed
還是unsigned
。
Implementation-defined、Unspecified和Undefined
在C標准中沒有做明確規定的地方會用Implementation-defined、Unspecified或Undefined來表述,在本書中有時把這三種情況統稱為“未明確定義”的。這三種情況到底有什么不同呢?
我們剛才看到一種Implementation-defined的情況,C標准沒有明確規定char
是有符號的還是無符號的,但是要求編譯器必須對此做出明確規定,並寫在編譯器的文檔中。
而 對於Unspecified的情況,往往有幾種可選的處理方式,C標准沒有明確規定按哪種方式處理,編譯器可以自己決定,並且也不必寫在編譯器的文檔中, 這樣即便用同一個編譯器的不同版本來編譯也可能得到不同的結果,因為編譯器沒有在文檔中明確寫它會怎么處理,那么不同版本的編譯器就可以選擇不同的處理方 式,比如下一章我們會講到一個函數調用的各個實參表達式按什么順序求值是Unspecified的。
Undefined的情況則是完全不確定的,C標准沒規定怎么處理,編譯器很可能也沒規定,甚至也沒做出錯處理,有很多Undefined的情況編譯器是檢查不出來的,最終會導致運行時錯誤,比如數組訪問越界就是Undefined的。
初 學者看到這些規則通常會很不舒服,覺得這不是在學編程而是在啃法律條文,結果越學越泄氣。是的,C語言並不像一個數學定理那么完美,現實世界里的東西總是 不夠完美的。但還好啦,C程序員已經很幸福了,只要嚴格遵照C標准來寫代碼,不要去觸碰那些陰暗角落,寫出來的代碼就有很好的可移植性。想想那些可憐的 JavaScript程序員吧,他們甚至連一個可以遵照的標准都沒有,一個瀏覽器一個樣,甚至同一個瀏覽器的不同版本也差別很大,程序員不得不為每一種瀏 覽器的每一個版本分別寫不同的代碼。
除了char
型之外,整型還包括short int
(或者簡寫為short
)、int
、long int
(或者簡寫為long
)、long long int
(或者簡寫為long long
)等幾種[25],這些類型都可以加上signed
或unsigned
關 鍵字表示有符號或無符號數。其實,對於有符號數在計算機中的表示是Sign and Magnitude、1's Complement還是2's Complement,C標准也沒有明確規定,也是Implementation Defined。大多數體系結構都采用2's Complement表示法,x86平台也是如此,從現在開始我們只討論2's Complement表示法的情況。還有一點要注意,除了char
型以外的這些類型如果不明確寫signed
或unsigned
關鍵字都表示signed
,這一點是C標准明確規定的,不是Implementation Defined。
除了char
型在C標准中明確規定占一個字節之外,其它整型占幾個字節都是Implementation Defined。通常的編譯器實現遵守ILP32或LP64規范,如下表所示。
表 15.1. ILP32和LP64
類型 | ILP32(位數) | LP64(位數) |
---|---|---|
char | 8 | 8 |
short | 16 | 16 |
int | 32 | 32 |
long | 32 | 64 |
long long | 64 | 64 |
指針 | 32 | 64 |
ILP32這個縮寫的意思是int
(I)、long
(L)和指針(P)類型都占32位,通常32位計算機的C編譯器采用這種規范,x86平台的gcc
也是如此。LP64是指long
(L)和指針占64位,通常64位計算機的C編譯器采用這種規范。指針類型的長度總是和計算機的位數一致,至於什么是計算機的位數,指針又是一種什么樣的類型,我們到第 17 章計算機體系結構基礎和第 23 章指針再分別詳細解釋。從現在開始本書做以下約定:在以后的陳述中,缺省平台是x86/Linux/gcc,遵循ILP32,並且char
是有符號的,我不會每次都加以說明,但說到其它平台時我會明確指出是什么平台。
在第 2 節 “常量”講過C語言的常量有整數常量、字符常量、枚舉常量和浮點數常量四種,其實字符常量和枚舉常量的類型都是int
型,因此前三種常量的類型都屬於整型。整數常量有很多種,不全是int
型的,下面我們詳細討論整數常量。
以前我們只用到十進制的整數常量,其實在C語言中也可以用八進制和十六進制的整數常量[26]。八進制整數常量以0開頭,后面的數字只能是0~7,例如022,因此十進制的整數常量就不能以0開頭了,否則無法和八進制區分。十六進制整數常量以0x或0X開頭,后面的數字可以是0~9、a~f和A~F。在第 6 節 “字符類型與字符編碼”講過一種轉義序列,以\或\x加八進制或十六進制數字表示,這種表示方式相當於把八進制和十六進制整數常量開頭的0替換成\了。
整數常量還可以在末尾加u或U表示“unsigned”,加l或L表示“long”,加ll或LL表示“long long”,例如0x1234U,98765ULL等。但事實上u、l、ll這幾種后綴和上面講的unsigned
、long
、long long
關鍵字並不是一一對應的。這個對應關系比較復雜,准確的描述如下表所示(出自[C99]條款6.4.4.1)。
表 15.2. 整數常量的類型
后綴 | 十進制常量 | 八進制或十六進制常量 |
---|---|---|
無 |
int |
int |
u或U |
unsigned int |
unsigned int |
l或L |
long int |
long int |
既有u或U,又有l或L |
unsigned long int |
unsigned long int |
ll或LL |
long long int |
long long int |
既有u或U,又有ll或LL |
unsigned long long int |
unsigned long long int |
給定一個整數常量,比如1234U,那么它應該屬於“u或U”這一行的“十進制常量”這一列,這個表格單元中列了三種類型unsigned int
、unsigned long int
、unsigned long long int
,從上到下找出第一個足夠長的類型可以表示1234這個數,那么它就是這個整數常量的類型,如果int
是32位的那么unsigned int
就可以表示。
再比如0xffff0000,應該屬於第一行“無”的第二列“八進制或十六進制常量”,這一列有六種類型int
、unsigned int
、long int
、unsigned long int
、long long int
、unsigned long long int
,第一個類型int
表示不了0xffff0000這么大的數,我們寫這個十六進制常量是要表示一個正數,而它的MSB(第31位)是1,如果按有符號int
類型來解釋就成了負數了,第二個類型unsigned int
可以表示這個數,所以這個十六進制常量的類型應該算unsigned int
。所以請注意,0x7fffffff和0xffff0000這兩個常量雖然看起來差不多,但前者是int
型,而后者是unsigned int
型。
講一個有意思的問題。我們知道x86平台上int
的取值范圍是-2147483648~2147483647,那么用printf("%d\n", -2147483648);
打印int
類型的下界有沒有問題呢?如果用gcc main.c -std=c99
編譯會有警告信息:warning: format ‘%d’ expects type ‘int’, but argument 2 has type ‘long long int’
。這是因為,雖然-2147483648這個數值能夠用int
型表示,但在C語言中卻沒法寫出對應這個數值的int
型常量,C編譯器會把它當成一個整數常量2147483648和一個負號運算符組成的表達式,而整數常量2147483648已經超過了int
型的取值范圍,在x86平台上int
和long
的取值范圍相同,所以這個常量也超過了long
型的取值范圍,根據上表第一行“無”的第一列十進制常量
,這個整數常量應該算long long
型的,前面再加個負號組成的表達式仍然是long long
型,而printf
的%d
轉換說明要求后面的參數是int
型,所以編譯器報警告。之所以編譯命令要加-std=c99
選項是因為C99以前對於整數常量的類型規定和上表有一些出入,即使不加這個選項也會報警告,但警告信息不准確,讀者可以試試。如果改成printf("%d\n", -2147483647-1);
編譯器就不會報警告了,-號運算符的兩個操作數-2147483647和1都是int
型,計算結果也應該是int
型,並且它的值也沒有超出int
型的取值范圍;或者改成printf("%lld\n", -2147483648);
也可以,轉換說明%lld
告訴printf
后面的參數是long long
型,有些轉換說明格式目前還沒講到,詳見第 2.9 節 “格式化I/O函數”。
怎么樣,整數常量沒有你原來想的那么簡單吧。再看一個不簡單的問題。long long i = 1234567890 * 1234567890;
編譯時會有警告信息:warning: integer overflow in expression
。1234567890是int
型,兩個int
型相乘的表達式仍然是int
型,而乘積已經超過int
型的取值范圍了,因此提示計算結果溢出。如果改成long long i = 1234567890LL * 1234567890;
,其中一個常量是long long
型,另一個常量也會先轉換成long long
型再做乘法運算,兩數相乘的表達式也是long long
型,編譯器就不會報警告了。有關類型轉換的規則將在第 3 節 “類型轉換”詳細介紹。
[25] 我們在第 4 節 “結構體和聯合體”還要介紹一種特殊的整型--Bit-field。
[26] 有些編譯器(比如gcc
)也支持二進制的整數常量,以0b或0B開頭,比如0b0001111,但二進制的整數常量從未進入C標准,只是某些編譯器的擴展,所以不建議使用,由於二進制和八進制、十六進制的對應關系非常明顯,用八進制或十六進制常量完全可以代替使用二進制常量。
2. 浮點型
C標准規定的浮點型有float
、double
、long double
,和整型一樣,既沒有規定每種類型占多少字節,也沒有規定采用哪種表示形式。浮點數的實現在各種平台上差異很大,有的處理器有浮點運算單元(FPU,Floating Point Unit),稱為硬浮點(Hard-float)實現;有的處理器沒有浮點運算單元,只能做整數運算,需要用整數運算來模擬浮點運算,稱為軟浮點(Soft-float)實現。大部分平台的浮點數實現遵循IEEE 754,float
型通常是32位,double
型通常是64位。
long double
型通常是比double
型精度更高的類型,但各平台的實現有較大差異。在x86平台上,大多數編譯器實現的long double
型是80位,因為x86的浮點運算單元具有80位精度,gcc
實現的long double
型是12字節(96位),這是為了對齊到4字節邊界(在第 4 節 “結構體和聯合體”詳細討論對齊的問題),也有些編譯器實現的long double
型和double
型精度相同,沒有充分利用x86浮點運算單元的精度。其它體系結構的浮點運算單元的精度不同,編譯器實現也會不同,例如PowerPC上的long double
型通常是128位。
以前我們只用到最簡單的浮點數常量,例如3.14,現在看看浮點數常量還有哪些寫法。由於浮點數在計算機中的表示是基於科學計數法的,所以浮點數常量也可以寫成科學計數法的形式,尾數和指數之間用e或E隔開,例如314e-2表示314×10-2,注意這種表示形式基數是10[27],如果尾數的小數點左邊或右邊沒有數字則表示這一部分為零,例如3.e-1,.987等等。浮點數也可以加一個后綴,例如3.14f、.01L,浮點數的后綴和類型之間的對應關系比較簡單,沒有后綴的浮點數常量是double
型的,有后綴f或F的浮點數常量是float
型的,有后綴l或L的浮點數常量是long double
型的。
[27] C99引入一種新的十六進制浮點數表示,基數是2,本書不做詳細介紹。
3. 類型轉換
如 果有人問C語法規則中最復雜的是哪一部分,我一定會說是類型轉換。從上面兩節可以看出,有符號、無符號整數和浮點數加起來有那么多種類型,每兩種類型之間 都要定義一個轉換規則,轉換規則的數量自然很龐大,更何況由於各種體系結構對於整數和浮點數的實現很不相同,很多類型轉換的情況都是C標准未做明確規定的 陰暗角落。雖然我們寫代碼時不會故意去觸碰這些陰暗角落,但有時候會不小心犯錯,所以了解一些未明確規定的情況還是有必要的,可以在出錯時更容易分析錯誤 原因。本節分成幾小節,首先介紹哪些情況下會發生類型轉換,會把什么類型轉成什么類型,然后介紹編譯器如何處理這樣的類型轉換。
在一個表達式中,凡是可以使用int
或unsigned int
類型做右值的地方也都可以使用有符號或無符號的char
型、short
型和Bit-field。如果原始類型的取值范圍都能用int
型表示,則其類型被提升為int
,如果原始類型的取值范圍用int
型表示不了,則提升為unsigned int
型,這稱為Integer Promotion。做Integer Promotion只影響上述幾種類型的值,對其它類型無影響。C99規定Integer Promotion適用於以下幾種情況:
1、如果一個函數的形參類型未知,例如使用了Old Style C風格的函數聲明(詳見第 2 節 “自定義函數”),或者函數的參數列表中有...,那么調用函數時要對相應的實參做Integer Promotion,此外,相應的實參如果是float
型的也要被提升為double
型,這條規則稱為Default Argument Promotion。我們知道printf
的參數列表中有...
,除了第一個形參之外,其它形參的類型都是未知的,比如有這樣的代碼:
char ch = 'A'; printf("%c", ch);
ch
要被提升為int
型之后再傳給printf
。
2、算術運算中的類型轉換。有符號或無符號的char
型、short
型和Bit-field在做算術運算之前首先要做Integer Promotion,然后才能參與計算。例如:
unsigned char c1 = 255, c2 = 2; int n = c1 + c2;
計算表達式c1 + c2
的過程其實是先把c1
和c2
提升為int
型然后再相加(unsigned char
的取值范圍是0~255,完全可以用int
表示,所以提升為int
就可以了,不需要提升為unsigned int
),整個表達式的值也是int
型,最后的結果是257。假如沒有這個提升的過程,c1 + c2
就溢出了,溢出會得到什么結果是Undefined,在大多數平台上會把進位截掉,得到的結果應該是1。
除了+號之外還有哪些運算符在計算之前需要做Integer Promotion呢?我們在下一小節先介紹Usual Arithmetic Conversion規則,然后再解答這個問題。
兩個算術類型的操作數做算術運算,比如a + b
,如果兩邊操作數的類型不同,編譯器會自動做類型轉換,使兩邊類型相同之后才做運算,這稱為Usual Arithmetic Conversion。轉換規則如下:
-
如果有一邊的類型是
long double
,則把另一邊也轉成long double
。 -
否則,如果有一邊的類型是
double
,則把另一邊也轉成double
。 -
否則,如果有一邊的類型是
float
,則把另一邊也轉成float
。 -
否則,兩邊應該都是整型,首先按上一小節講過的規則對
a
和b
做Integer Promotion,然后如果類型仍不相同,則需要繼續轉換。首先我們規定char
、short
、int
、long
、long long
的轉換級別(Integer Conversion Rank)一個比一個高,同一類型的有符號和無符號數具有相同的Rank。轉換規則如下:-
如果兩邊都是有符號數,或者都是無符號數,那么較低Rank的類型轉換成較高Rank的類型。例如
unsigned int
和unsigned long
做算術運算時都轉成unsigned long
。 -
否則,如果一邊是無符號數另一邊是有符號數,無符號數的Rank不低於有符號數的Rank,則把有符號數轉成另一邊的無符號類型。例如
unsigned long
和int
做算術運算時都轉成unsigned long
,unsigned long
和long
做算術運算時也都轉成unsigned long
。 -
剩下的情況是:一邊有符號另一邊無符號,並且無符號數的Rank低於有符號數的Rank。這時又分為兩種情況,如果這個有符號數類型能夠覆蓋這個無符號數類型的取值范圍,則把無符號數轉成另一邊的有符號類型。例如遵循LP64的平台上
unsigned int
和long
在做算術運算時都轉成long
。 -
否則,也就是這個有符號數類型不足以覆蓋這個無符號數類型的取值范圍,則把兩邊都轉成有符號數的Rank對應的無符號類型。例如在遵循ILP32的平台上
unsigned int
和long
在做算術運算時都轉成unsigned long
。
-
可 見有符號和無符號整數的轉換規則是十分復雜的,雖然這是有明確規定的,不屬於陰暗角落,但為了程序的可讀性不應該依賴這些規則來寫代碼。我講這些規則,不 是為了讓你用,而是為了讓你了解有符號數和無符號數混用會非常麻煩,從而避免觸及這些規則,並且在程序出錯時記得往這上面找原因。所以這些規則不需要牢 記,但要知道有這么回事,以便在用到的時候能找到我書上的這一段。
到目前為止我們學過的+ - * / % > < >= <= == !=運算符都需要做Usual Arithmetic Conversion,因為都要求兩邊操作數的類型一致,在下一章會介紹幾種新的運算符也需要做Usual Arithmetic Conversion。單目運算符+ - ~只有一個操作數,移位運算符<< >>兩邊的操作數類型不要求一致,這些運算不需要做Usual Arithmetic Conversion,但也需要做Integer Promotion,運算符~ << >>將在下一章介紹。
如果賦值或初始化時等號兩邊的類型不相同,則編譯器會把等號右邊的類型轉換成等號左邊的類型再做賦值。例如int c = 3.14;
,編譯器會把右邊的double
型轉成int
型再賦給變量c
。
我們知道,函數調用傳參的過程相當於定義形參並且用實參對其做初始化,函數返回的過程相當於定義一個臨時變量並且用return
的表達式對其做初始化,所以由賦值產生的類型轉換也適用於這兩種情況。例如一個函數的原型是int foo(int, int);
,則調用foo(3.1, 4.2)
時會自動把兩個double
型的實參轉成int
型賦給形參,如果這個函數定義中有返回語句return 1.2;
,則返回值1.2
會自動轉成int
型再返回。
在函數調用和返回過程中發生的類型轉換往往容易被忽視,因為函數原型和函數調用並沒有寫在一起。例如char c = getchar();
,看到這一句往往會想當然地認為getchar
的返回值是char
型,而事實上getchar
的返回值是int
型,這樣賦值會引起類型轉換,可能產生Bug,我們在第 2.5 節 “以字節為單位的I/O函數”詳細討論這個問題。
以上三種情況通稱為隱式類型轉換(Implicit Conversion,或者叫Coercion),編譯器根據它自己的一套規則將一種類型自動轉換成另一種類型。除此之外,程序員也可以通過類型轉換運算符(Cast Operator)自己規定某個表達式要轉換成何種類型,這稱為顯式類型轉換(Explicit Conversion)或強制類型轉換(Type Cast)。例如計算表達式(double)3 + i
,首先將整數3強制轉換成double
型(值為3.0),然后和整型變量i
相加,這時適用Usual Arithmetic Conversion規則,首先把i
也轉成double
型,然后兩者相加,最后整個表達式也是double
型的。這里的(double)
就是一個類型轉換運算符,這種運算符由一個類型名套()括號組成,屬於單目運算符,后面的3是這個運算符的操作數。注意操作數的類型必須是標量類型,轉換之后的類型必須是標量類型或者void
型。
以上幾小節介紹了哪些情況下會發生類型轉換,並且明確了每種情況下會把什么類型轉成什么類型,本節介紹編譯器如何處理任意兩種類型之間的轉換。現在要把一個M位的類型(值為X)轉換成一個N位的類型,所有可能的情況如下表所示。
表 15.3. 如何做類型轉換
待轉換的類型 | M > N的情況 | M == N的情況 | M < N的情況 |
---|---|---|---|
signed integer to signed integer | 如果X在目標類型的取值范圍內則值不變,否則Implementation-defined | 值不變 | 值不變 |
unsigned integer to signed integer | 如果X在目標類型的取值范圍內則值不變,否則Implementation-defined | 如果X在目標類型的取值范圍內則值不變,否則Implementation-defined | 值不變 |
signed integer to unsigned integer | X % 2N | X % 2N | X % 2N |
unsigned integer to unsigned integer | X % 2N | 值不變 | 值不變 |
floating-point to signed or unsigned integer | Truncate toward Zero,如果X的整數部分超出目標類型的取值范圍則Undefined | ||
signed or unsigned integer to floating-point | 如果X在目標類型的取值范圍內則值不變,但有可能損失精度,如果X超出目標類型的取值范圍則Undefined | ||
floating-point to floating-point | 如果X在目標類型的取值范圍內則值不變,但有可能損失精度,如果X超出目標類型的取值范圍則Undefined | 值不變 | 值不變 |
注意上表中的“X % 2N”,我想表達的意思是“把X加上或者減去2N的整數倍,使結果落入[0, 2N-1]的范圍內”, 當X是負數時運算結果也得是正數,即運算結果和除數同號而不是和被除數同號,這不同於C語言%運算的定義。寫程序時不要故意用上表中的規則,尤其不要觸碰 Implementation-defined和Undefined的情況,但程序出錯時可以借助上表分析錯誤原因。
下面舉幾個例子說明上表的用法。比如把double
型轉換成short
型,對應表中的“floating-point to signed or unsigned integer”,如果原值在(-32769.0, 32768.0)之間則截掉小數部分得到轉換結果,否則產生溢出,結果是Undefined,例如對於short s = 32768.4;
這個語句gcc
會報警告。
比如把int
型轉換成unsigned short
型,對應表中的“signed integer to unsigned integer”,如果原值是正的,則把它除以216取模,其實就是取它的低16位,如果原值是負的,則加上216的整數倍,使結果落在[0, 65535]之間。
比如把int
類型轉換成short
類型,對應表中的“signed integer to signed integer”,如果原值在[-32768, 32767]之間則值不變,否則產生溢出,結果是Implementation-defined,例如對於short s = -32769;
這個語句gcc
會報警告。
最后一個例子,把short
型轉換成int
型,對應表中的“signed integer to signed integer”,轉換之后應該值不變。那怎么維持值不變呢?是不是在高位補16個0就行了呢?如果原值是-1,十六進制表示就是ffff,要轉成int
型的-1需要變成ffffffff,因此需要在高位補16個1而不是16個0。換句話說,要維持值不變,在高位補1還是補0取決於原來的符號位,這稱為符號擴展(Sign Extension)。