16 進制的常量通常都用作掩碼或特殊位的值。如果一個沒有后綴的 16 進制的常量是 32 位的,并且其高位被置位了,那么它就可以作為無符號整型進行定義。
例如,常數 OxFFFFFFFFL 是一個有符號的 long 類型。在 32 位系統上,這會將所有位都置位(每位全為 1),但是在 64 位系統上,只有低 32 位被置位了,結果是這個值是 0x00000000FFFFFFFF.
如果我們希望所有位全部置位,那么一種可移植的方法是定義一個有符號的常數,其值為 -1.這會將所有位全部置位,因為它采用了二進制補碼算法。
|
|
Endianism 是指用來存儲數據的方法,它定義了整數和浮點數據類型中是如何對字節進行尋址的。
Little-endian 是將低位字節存儲在內存的低地址中,將高位字節存儲在內存的高地址中。
Big-endian 是將高位字節存儲在內存的低地址中,將低位字節存儲在內存的高地址中。
表 3 給出了一個 64 位長整數的布局示例。
表 3. 64 位 long int 類型的布局
低地址 | 高地址 | |||||||
---|---|---|---|---|---|---|---|---|
Little endian | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 | Byte 6 | Byte 7 |
Big endian | Byte 7 | Byte 6 | Byte 5 | Byte 4 | Byte 3 | Byte 2 | Byte 1 | Byte 0 |
表 4. 0x12345678 在 big-endian 系統上的布局
內存偏移量 | 0 | 1 | 2 | 3 |
內存內容 | 0x12 | 0x34 | 0x56 | 0x78 |
如果將 0x12345678 當作兩個半字來看待,分別是 0x1234 和 0x5678,那么就會看到在 big endian 機器上是下面的情況:
表 5. 0x12345678 在 big-endian 系統上當作兩個半字來看待的情況
內存偏移量 | 0 | 2 |
內存內容 | 0x1234 | 0x5678 |
表 6. 0x12345678 在 little-endian 系統上的布局
內存偏移量 | 0 | 1 | 2 | 3 |
內存內容 | 0x78 | 0x56 | 0x34 | 0x12 |
表 7. 0x12345678 在 little-endian 系統上作為兩個半字看到的情況
內存偏移量 | 0 | 2 |
內存內容 | 0x3412 | 0x7856 |
下面的 C 程序在一臺 big endian 機器上進行編譯和運行時會打印 “Big endian”,在一臺 little endian 機器上進行編譯和運行時會打印 “Little endian”。
清單 2. big endian 與 little endian
|
使用位掩碼時
對象的間接指針地址部分
在 C 和 C++ 中有位域來幫助處理 endian 的問題。我建議使用位域,而不要使用掩碼域或 16 進制的常量。有幾個函數可以用來將 16 位和 32 位數據從 “主機字節順序” 轉換成 “網絡字節順序”。例如,htonl (3)、ntohl (3) 用來轉換 32 位整數。類似地,htons (3)、ntohs (3) 用來轉換 16 位整數。然而,對于 64 位整數來說,并沒有標準的函數集。但是在 big endian 和 little endian 系統上,Linux 都提供了下面的幾個宏:
bswap_16
bswap_32
bswap_64
類型定義
建議您不要使用 C/C++ 中那些在 64 位系統上會改變大小的數據類型來編寫應用程序,而是使用一些類型定義或宏來顯式地說明變量中所包含的數據的大小和類型。有些定義可以使代碼的可移植性更好。
ptrdiff_t:
這是一個有符號整型,是兩個指針相減后的結果。
size_t:
這是一個無符號整型,是執行 sizeof 操作的結果。這在向一些函數(例如 malloc (3))傳遞參數時使用,也可以從一些函數(比如 fred (2))中返回。
int32_t、uint32_t 等:
定義具有預定義寬度的整型。
intptr_t 和 uintptr_t:
定義整型類型,任何有效指針都可以轉換成這個類型。
例 1:
在下面這條語句中,在對 bufferSize 進行賦值時,從 sizeof 返回的 64 位值被截斷成了 32 位。
int bufferSize = (int) sizeof (something);
解決方案是使用 size_t 對返回值進行類型轉換,并將其賦給聲明為 size_t 類型的 bufferSize,如下所示:
size_t bufferSize = (size_t) sizeof (something);
例 2:
在 32 位系統上,int 和 long 大小相同。由于這一點,有些開發人員會交換使用這兩種類型。這可能會導致指針被賦值給 int 類型,或者反之。但是在 64 位的系統上,將指針賦值給 int 類型會導致截斷高 32 位的值。
解決方案是將指針作為指針類型或為此而定義的特殊類型進行存儲,例如 intptr_t 和 uintptr_t.
位移
無類型的整數常量就是 (unsigned) int 類型的。這可能會導致在位移時出現被截斷的問題。
例如,在下面的代碼中,a 的最大值可以是 31.這是因為 1 << a 是 int 類型的。
long t = 1 << a;
要在 64 位系統上進行位移,應該使用 1L,如下所示:
long t = 1L << a;
字符串格式化
函數 printf (3) 及其相關函數都可能成為問題的根源。例如,在 32 位系統上,使用 %d 來打印 int 或 long 類型的值都可以,但是在 64 位平臺上,這會導致將 long 類型的值截斷成低 32 位的值。對于 long 類型的變量來說,正確的用法是 %ld.
類似地,當一個小整數(char、short、int)被傳遞給 printf (3) 時,它會擴展成 64 位的,符號會適當地進行擴展。在下面的例子中,printf (3) 假設指針是 32 位的。
char *ptr = &something;printf (%x\n", ptr);
上面的代碼在 64 位系統上會失敗,它只會顯示低 4 字節的內容。
這個問題的解決方案是使用 %p,如下所示;這在 32 位和 64 位系統上都可以很好地工作:
char *ptr = &something;printf (%p\n", ptr);
函數參數
在向函數傳遞參數時需要記住幾件事情:
在參數的數據類型是由函數原型定義的情況中,參數應該根據標準規則轉換成這種類型。
在參數類型沒有指定的情況中,參數會被轉換成更大的類型。
在 64 位系統上,整型被轉換成 64 位的整型值,單精度的浮點類型被轉換成雙精度的浮點類型。
如果返回值沒有指定,那么函數的缺省返回值是 int 類型的。
在將有符號整型和無符號整型的和作為 long 類型傳遞時就會出現問題??紤]下面的情況:
清單 3. 將有符號整型和無符號整型的和作為 long 類型傳遞
|
上面這段代碼在 64 位系統上會失敗,因為表達式 (i + k) 是一個無符號的 32 位表達式,在將其轉換成 long 類型時,符號并沒有得到擴展。解決方案是將一個操作數強制轉換成 64 位的類型。
在基于寄存器的系統上還有一個問題:系統采用寄存器而不是堆棧來向函數傳遞參數??紤]下面的例子:
float f = 1.25;
printf ("The hex value of %f is %x", f, f);
在基于堆棧的系統中,這會打印對應的 16 進制值。但是在基于寄存器的系統中,這個 16 進制的值會從一個整數寄存器中讀取,而不是從浮點寄存器中讀取。
解決方案是將浮點變量的地址強制轉換成一個指向整型類型的指針,如下所示:
printf ("The hex value of %f is %x", f, *(int *)&f);
結束語
主流的硬件供應商最近都在擴充自己的 64 位產品,這是因為 64 位平臺可以提供更好的性能、價值和可伸縮性。32 位系統的限制,特別是 4GB 的虛擬內存上限,已經極大地刺激很多公司開始考慮遷移到 64 位平臺上。了解如何將應用程序移植到 64 位體系結構上可以幫助我們編寫可移植性更好且效率更高的代碼。
關于作者
Harsha Adiga 就職于印度的 IBM Software Group,他參與了很多 Linux 和開放源碼社區、工作組的工作。