12可移植性(相容性)
本章討論C++可移植性問題主要關註:32位移植到64位,不同CPU架構之間的移植。
移植中一些關鍵問題如下:
1. 指標截斷
2. 資料類型位元組對齊
3. 對記憶體位址的錯誤假設
4. 對複合資料型別成員地址的錯誤假設
5. 大小端,網路位元組序問題
建議12.1 不直接使用C++的基礎資料型別 (Elementary Data Type),不要假定其儲存尺寸長度
說明:C++標準沒有明確基礎資料型別 (Elementary Data Type)的大小與儲存格式,這些基本類型包括:short,int, long, long
long, float double等。這些基礎資料型別 (Elementary Data Type)在不同的編譯器中,實現有所不同,如:
long類型在32位編譯模式下為4位元組長度,在64位編譯模式下為8位元組長度。
所以建議不要直接使用基礎資料型別 (Elementary Data Type)。推薦如下兩種使用方式:
1、重定義基礎資料型別 (Elementary Data Type)
typedef int32_t int;
typedef int64_t long long;
使用重定義後的基本類型好處是:如果程式需要移植,可以大大減少移植的工作量。
2、使用C99標準中定義的標準類型
int64_t my_value = 0x123456789LL;
uint64_t my_mask = 3ULL << 48;
使用這些標準類型長度的好處是,它們規定了固定的長度,這個長度不會隨編譯器變化而變化,所以
我們可以放心的使用。
建議12.2 避免指標截斷
說明:指標截斷是從32位移植到64位系統時,經常會碰到的問題。
int *i = &int_val;
short *w = (short*)((int)i + 2);
上面的代碼在32位環境下運行是沒有問題的,但在64位環境下,發生了地址截斷:無法把64位長的數
據接到32位的資料空間裡面。
上面的代碼中可以使用intptr_t類型解決:
int *i = &int_val;
short *w = (short*)((intptr_t)i + 2);
建議12.3 注意資料類型對齊問題
說明:需要對結構對齊加以留心,尤其是對於儲存在磁碟上的結構體。
在64位系統中,任何擁有int64_t/uint64_t成員的類/結構體將預設被處理為8位元組對齊。如果32位和64 位
代碼共用磁碟上的結構體,需要確保兩種體繫結構下的結構體的一致對齊。
另外,大多數編譯器提供了調整結構體對齊的方案:
gcc 中可使用__attribute__((packed)),MSVC 提供了#pragma pack()和__declspec(align()) 。
由於各個平台和編譯器的不同,所以在不同編譯器與平台移植代碼時,一定要特別關注編譯器關於對
齊的參數設定與預設值。因為位元組對齊不僅影響效能,而且會導致一些不可預知的問題。
建議12.4 在涉及網路位元組序處理時,要注意進行網路位元組序與本地位元組序的轉換
說明:小端法(Little-Endian)
低位位元組排放在記憶體的低地址端即起始地址,高位位元組排放在記憶體的高地址端。
大端法(Big-Endian)
高位位元組排放在記憶體的低地址端即起始地址,低位位元組排放在記憶體的高地址端。
不同cpu平台上位元組序通常也不一樣:
X86、AMD64平台使用小端法、而HP-IA, IBM AIX的CPU採用的是大端法。
而網路位元組序是大端法,如常見網路發送的碼流。
涉及網路位元組需要注意處理網路位元組序與本地位元組序的轉換,即使本地位元組序採用的也是大端法,為
了程式可移植性,建議也調用轉換函式進行轉換。
庫函數提供了16,32位整型int的網路位元組序與本地位元組序的轉換函式:
htonl, htons, ntohl, ntohs - convert values between host and network byte order
建議12.5 避免無符號數與有符號數的轉換
說明:不同的國際標準(ANSI C/ISO C++等)對隱式轉換有符號和無符號類型的規則不同,有可能導致
不同的執行結果。
unsigned short usNumber = xxx;
long lNum = usNumber;
將unsigned short賦值給long需要經過兩次類型轉換,ANSI標準中沒有規定多次類型轉換的順序。大多
數編譯器(例如VC)在高位優先填充0,按照下面的順序進行轉換:
lNum = (long) (unsigned long) usNumber;
個別編譯器(例如BSD的一些編譯器)在高位優先填充1,即使用下面的順序進行轉換:
lNum = (long) (signed short) usNumber;
如果是後一種轉換順序,並且正好usNumber 的高位為1,則首先被轉換成一個負數的long,接著轉換成
unsigned long時就成了很大的數。
usNumber永遠不可能為負數,沒有必要使用signed修飾。
修改辦法是定義lNum的類型為unsigned long,並更改名字為ulNum:
unsigned short usNumber = xxx;
unsigned long ulNum = usNumber;
盡量避免無符號數與有符號數的轉換,特別是長度不同數值的類型轉換。請首先考慮設計上是否需要
這種轉換。
建議12.6 建立64 位元常量時使用LL 或ULL 作為尾碼
說明:指定LL或ULL尾碼說明,能讓代碼更加清晰。
int64_t my_value = 0x123456789LL;
uint64_t my_mask = 3ULL << 48;
尤其是在>>操作時,無符號與有符號有很大差異的,如果運算元是無符號數,則右移操作符>>,從左
邊開始插入0, 否則插入符號位的拷貝或者插入0 ,這由編譯器決定。
建議12.7 區分sizeof(void *)和sizeof(int)
說明:64位下sizeof(void *) != sizeof(int),而在32位下是相同的。
如果需要一個指標大小的整數請使用intptr_t。
建議12.8 編譯器,作業系統相關的代碼獨立出來
說明:為了程式的可移植性,建議將編譯器,作業系統相關的代碼從產品代碼中獨立出來。
編譯器特有的東西,如gcc的編譯參數__thread, __attribute__等,如果需要做到支援多個平台,需
要封裝為宏或函數。
例如gcc的__thread對應的VC開關為__declspec(thread)
__thread int number;
__declspec(thread) int number;
均表示number為線程私人儲存變數,可採用如下宏來封裝:
#ifdef WIN32
#define THREAD __declspec(thread)
#else
#define THREAD __thread
#endif
THREAD int number;