C語言中的型別轉換與資料的機器碼儲存
各種型別的表示範圍
對於涉及到了混合著不同資料型別的表示式中的資料型別的轉換問題。在總結轉換問題之前,先說明一下32位機上的各種資料型別。
型別名 |
位元組數 |
其他型別名 |
10進製表示範圍(機器碼) |
char |
1 |
signed char |
-128—127(0X80-0X7F) |
unsigned char |
1 |
none |
0-255(0X00-0XFF) |
short |
2 |
short int signed short int |
-32768—32767 (0X8000-0X7FFF) |
unsigned short |
2 |
unsigned short |
0-65535 (0X0000-0XFFFF) |
int |
4 |
signed int |
-2147483648—2147483647 (0X80000000-0X7FFFFFFF) |
unsigend int |
4 |
none |
0—4294967295 (0X80000000-0XFFFFFFFF) |
long |
4 |
long int signed long int |
-2147483648—2147483647 (0X80000000-0X7FFFFFFF) |
unsigned long |
4 |
unsigned long int |
0—4294967295 (0X00000000-0XFFFFFFFF) |
long long |
8 |
signed long long int signed long long long long int |
0X8000000000000000- 0X7FFFFFFFFFFFFFFF |
unsigned long long |
8 |
unsigned long long int |
0X0000000000000000- 0XFFFFFFFFFFFFFFFF |
enum |
4 |
none |
same as int |
float |
4 |
none |
3.4E +/- 38 (7 digits) |
double |
8 |
none |
1.7E +/- 308 (15 digits) |
說明一下:
1)在32位機上,int型和unsigned int型都是32位的(4個位元組);
2)
3)關於型別的大小。一般用所能表示的資料範圍來比較型別的大小,如char型<unsigned char型<short型,在表示式中,一般都是由小的型別向大的型別轉換(強制型別轉換除外)。
4)-128的補碼錶示是0X80,二進位制寫法是10000000,補碼能直接參與加減運算,如-128+127=-1,用補碼進位制運算:10000000+01111111 = 11111111,即0XFF恰好是-1的補碼。
關於型別轉換
下面總結一下關於型別轉換(僅限於算術表示式中整型的轉換)的原則:
1)所有比int型小的資料型別(包括char、signed char、unsigned char、short、signed short、unsigned short)轉換為int型。如果轉換後的資料會超出int型所能表示的範圍的話,則轉換為unsigned int型;
2)bool型轉化為int型時,false轉化為0,true轉換為1;反過來所有的整數型別轉化為bool時,0轉化為false,其它非零值都轉為true;
3)如果表示式中混有unsigned short和int型時,如果int型資料可以表示所有的unsigned short型的話,則將unsigned short型別的資料轉換為int型;否則unsigned short型別及int型都轉換為unsigned int型別。舉個例子,在32位機上,int是32位,範圍–2147483648——2147483647;unsigned short是16位,範圍是0——65535。這樣int型足夠表示unsigned short型別的資料,因此在混有這兩者的運算中,unsigned short型別資料被轉換為int型;
4)unsigned int與long型別的轉換規律同3,在32位機上,unsigned int是32位,範圍0 ——4294967295,long也是32位,範圍–2147483648 ——2147483647,可見long型別不夠表示所有的unsigned int型,因此在混有unsigned int及long的表示式中,兩者都被轉換為unsigned long,值得注意的是32位機上unsigned long也佔用4位元組空間;
5)如果表示式中既有int 又有unsigned int,則所有的int資料都被轉化為unsigned int型別;
6)在計算機中,負數是以補碼來儲存的。
經典的測試程式碼32位機上測試程式碼如下:
int main(void)
{
{ //(1)
unsigned char a = -1;
char b = a;
printf("a = %d,b = %d\n",a,b); // a = 255,b = -1
}
{ //(2)
unsigned short a = -1;
short b = a;
printf("a = %d,b = %d\n",a,b); //a =65535,b = -1
}
{ //(3)
unsigned int a = -1;
int b = a;
printf("a = %u,b = %d\n",a,b); //a =65535,b = -1
printf("a = %d,b = %d\n",a,b); //a = -1,b = -1
}
{ //(4)
signed char a = 0XE0;
unsigned int b = a;
unsigned char c = a;
printf("a = %d,b = %d,c = %d\n",a,b,c); //a = -32,b = -32,c = 224
printf("a = %d,b = %u,c = %d\n",a,b,c); //a = -32,b = 4294967264,c = 224
}
{ //(5)
unsigned int i=3;
printf("i*(-1) = %d\n",i*(-1)); //i*(-1) = -3
printf("i*(-1) = %u\n",i*(-1)); //i*(-1) = 4294967293
std::cout<<i*(-1)<<std::endl; //4294967293
}
{ //(6)
unsigned int A = 1;
unsigned int B = 3;
unsigned int C = A - B;
unsigned int D = 4;
if(A-B >= D) //4294967294 >= 4
printf("aaaaaaaaaaaaaaaaaa\n"); //aaaaaaaaaaaaaaaaaa
printf("C = %u\n",C); //C = 4294967294
printf("C = %d\n",C); //C = -2
printf("C = %X\n",C); //C = FFFFFFFE
}
{ //(7)
char a[1000] ;
int i ;
for(i=0; i<1000; i++)
{
a[i]= -1-i ;
}
printf("strlen(a) = %u\n",strlen(a)) ; //strlen(a) = 255
}
return 0;
}
程式碼分析:
對於(1),C語言中常量整型數-1的補碼錶示為0XFFFFFFFF。擷取後面8位FF賦值給變數a(unsigned char),此時a = 0XFF(a沒有符號位,0XFF轉換為十進位制為255),又將0XFF,直接賦值給char b,此時b = 0XFF(但是要注意,b是有符號的,0XFF是一個負數的補碼錶示,轉換為十進位制為整數-1)。
執行語句printf("a = %d,b = %d\n",a,b)的時候,要將 a和b的值先轉換為int型:a沒有符號所以轉為int型為0x0000FF,b有符號轉換為int型為0xFFFFFFFF。十進位制整型數輸出值為a =255,b = -1
對於(2),擷取後面16位FFFF賦值給變數a(unsigned short)。此時a = 0XFFFF(a沒有符號位,0XFFFF轉換為十進位制為65535),又將0XFFFF,直接賦值給short b。 此時b = 0XFFFF(但是要注意,b是有符號的,0XFFFF是一個負數的補碼錶示,轉換為十進位制為整數-1)。
執行語句printf("a = %d,b = %d\n",a,b)的時候,要將 a和b的值先轉換為int型:a沒有符號所以轉為int型為0x0000FFFF,b有符號轉換為int型為0xFFFFFFFF。十進位制整型數輸出值為a =65535,b = -1
對於(3),a在記憶體中值為0XFFFFFFFF,b的值為0XFFFFFFFF,都已經32位,a轉換為int型的時候就是0XFFFFFFFF,所以輸出都是-1。
對於(4),a賦值給b時,首先是signed char轉換為int,然後int轉換成unsigned int,所以最初是符號擴充套件,1位元組的0XE0擴充套件成4位元組的0XFFFFFFE0然後一個int賦值給了unsigned int,都是32位,b在記憶體中就是0XFFFFFFE0。a以有符號整型的形式列印,0XFFFFFFE0是補碼,原碼是0X80000020,十進位制表示就是-32,若以符號整型數的形式列印,0XFFFFFFE0是原碼,十進位制表示就是4294967264,c在記憶體中表示是0XE0,無符號數,以%d形式列印先轉換為int型的時候就是0X000000E0,十進位制表示是224。
對於(5),在表示式i*-1中,i是unsigned int型,-1是int型(常量整數的型別等同於enum),-1必須轉換為unsigned int型,即0XFFFFFFFF,十進位制的4294967295,然後再與i相乘,即4294967295*3,如果不考慮溢位的話,結果是12884901885,十六進位制0X2FFFFFFFD,由於unsigned int只能表示32位,因此結果是0XFFFFFFFD,即4294967293。
對於(6),該段測試了無符號數減法運算的溢位現象,兩個無符號數相減,無論是否溢位,結果一定依然是一個無符號的正數。首先作純算術運算得到1-3=-2,-2以int型的補碼形式儲存在記憶體中是0XFFFFFFFE,-2是int型,將其賦值給unsigned int型的C時,必須轉換成unsigned int型,都是32位,C在記憶體中就是0XFFFFFFFE,A-B >= D中兩邊都是unsigned int型,0XFFFFFFFE以unsigned int型讀取是正數4294967294,遠大於4。C以%d形式列印,即轉換為int型,把C當作int型資料,0XFFFFFFFE是-2的補碼,故打印出C = -2。
對於(7),char型能夠表示的數值範圍是-128—127,即0X80—0X7F。按照負數補碼規則,可知-1的補碼為0XFF,-2的補碼是0XFE…,當i值為127時,a[127]的值為-128,此時右邊整型數0XFFFFFF80轉換後,正好是左邊char型能夠表示的最小負數0X80。當i繼續增加,右邊為-129,其對應的十六進位制補碼是0XFFFFFF7F,而char只有8位,故轉換時高位被丟棄,左邊得到0X7F,正好是char型能夠表示的最大正數127。當i繼續增加到255時,右邊整型數-256的補碼是0XFFFFF100(正數256的原碼是0X00000100,其反碼是0XFFFFF0FF,故-256的補碼是0XFFFFF100),低8位為0,a[255] = 0。然後當i增加到256時,-257(補碼是0XFFFFFEFF)的低8位為0XFF,即a[256] = 0XFF(0XFF是-1的補碼),如此又開始一輪的迴圈。
32位小端機上下面程式的輸出結果是什麼
int main()
{
long long a = 1, b = 2, c = 3;
printf("%d %d %d\n", a, b, c); //1 0 2
return 0;
}
分析:首先,sprintf/fprintf/printf/sscanf/fscanf/scanf等這一類的函式,它們的呼叫規則(calling conventions)是cdecl,cdecl呼叫規則的函式,所有引數從右到左依次入棧,這些引數由呼叫者清除,稱為手動清棧。被呼叫函式不會要求呼叫者傳遞多少引數,呼叫者傳遞過多或者過少的引數,甚至完全不同的引數都不會產生編譯階段的錯誤。函式引數的傳遞都是放在棧裡面的,而且是從右邊的引數開始壓棧,printf()是不會對傳遞的引數進行型別檢查的,它只有一個format specification fields的字串,而引數是不定長的,所以不能對傳遞的引數做型別檢查,也不能對引數的個數進行檢查。在壓棧的時候,引數列表裡的所有引數都壓入棧中了,它不知道有多少個引數。
編譯器是怎麼去定義壓棧的行為的?是先把這long long型別轉換為int型再壓棧?還是直接壓棧?
在32位機器上,64位的整數被拆分為兩個32位整數,printf會把64位的按照兩個32的引數來處理。此時printf會認為實際的引數為6個,而不是3個。
c、b、a壓棧之後,在最低的12位元組處是a和b,a佔2*4個bytes,b佔1*4個byte。b先壓入棧,a後壓入棧。因為是小端機,即每個數字的高位元組在高地址,低位元組在低地址。而棧的記憶體生長方向是從大到小的,也就是棧底是高地址,棧頂是低地址,所以a的低位元組在低地址。
那麼輸出的時候,format specification fields字串匹配棧裡面的內容,首先一個%d取出4個bytes出來輸出,然後後面又有一個%d再取出4個bytes出來列印。所以結果就是這樣了。也就是說剛開始壓入棧的c的值在輸出的時候根本都沒有用到。
輸出:1 0 2
引申:輸出格式符說明
%lu 輸出無符號10進位制長整型數;
%u 輸出無符號10進位制整型數;
%ld用來輸出10進位制長整型數;
%d用來輸出10進位制int整型數;
%f用來輸出10進位制單精度、雙精度浮點數,預設輸出6位小數。
格式符指明瞭輸出指定的起始地址開始的若干個位元組的內容(把它們作為長整型數或整型數來解釋),如果用錯了物件,就會得出意想不到的結果。