一,????????????
為什么要跨平臺?
你想過把你的
Windows
上編寫的程序在
Linux
編譯運行嗎,以及在
Mac
或其他
OS
上運行等等?反過來也一樣?這就需要涉及到跨平臺編程知識。這里需要注意的是,平時很多在一個平臺運行的程序在跨平臺的時候變的不再正確。
Java
并非真的是跨平臺的開發環境,它是運行在它自己的平臺上。這里主要關注
C
和
C++
的跨平臺開發。
下面主要就幾個方面來討論跨平臺編程的注意事項:
1.?
字節序
2.?
字節填充
3.?
其他
二,????????????
字節序
大家都知道計算機使用兩種字節序,一種是
little-endian
,另一種是
big-endian
。這主要是由于當前流行的
CPU
之間的差異造成的,基本上是
IBM-PowerPC
使用的大序,而其他
CPU
使用的小序。
這里先來介紹一下
little-endian
和
big-endian
之間的具體差異。
X86
指令集合使用小序(
little-endian
)字節順序;這就意味著多個字節值的最重要字節在地址的最低位。小序很早就使用,因為硬件容易實現,但和今天的制造商技術有點不同;但在第一代
IBM PC
機的
Vaxen
和
8086
處理器使用是它如此流行的主要原因。
看一個例子:
short example[2] = {0x0001,0x3002};
|
?
按照
16
進制的形式來顯示上面數據在內存中的存儲方式:
我們看到對于數組的第一個元素,高
8
位應該是
0
,而最終存儲的時候是在低
8
位的后面。
而另一方面
PowerPC
和
Sparc
芯片是
big-endian
的,也就是說,最重要的字節存儲在較低的地址。對于
CPU
需要額外的電路實現這個功能,但對于今天的處理器技術與緩存控制技術相比較顯的微不足道。使用
BIG-ENDIAN
的最大好處是在使用低級調式器時比較容易理解數據的存儲,同樣對于文件十六進制
DUMP
或網絡
Sniffer
顯示也是一樣的。
對于
BIG-ENDIAN
,上面的例子中內存如下表示:
這里需要注意的是:由于
BIG-ENDIAN
格式的
RAW
數據比較容易調式,如果我們有機會設計一個新的文件格式,那么使用
BIG-ENDIAN
格式,而不是根據
CPU
架構來決定。
下面看幾個關于字節序的問題:
1.?
Long
型指針和
char
指針之間的轉換
看下面這段代碼
unsigned long value = 0x03020100;
unsigned long *ptr = &value;
unsigned char charVal;
charVal = *(unsigned char *)ptr;
|
程序的含義比較簡單,主要是從一個指向
long
的指針強制轉換為一個指向
char
的指針,這里假設指針指向的是最不重要的字節地址。
在一個
little-endian
處理器上,
charVal
是
0
,而在一個
big-endian
處理器上,
charVal
的值是
3
。這樣的問題是最難以發現的問題之一。
為了避免這個錯誤,使用一個臨時變量可以解決這個問題,如下:
unsigned long temp = *ptr;
charVal = (unsigned char)temp;
|
上面的第二行代碼就保證將在任何架構上都將最不重要的字節傳遞給
charVal
;編譯器處理具體的細節。
2.?
讀寫文件和寫網絡數據
在從文件讀數據或寫數據到文件的時候以及網絡,對于字節順序的處理一定要小心;一定記住不能將多個字節的數據寫到文件或網絡上;例如:
long val = 1;
int result = write(fileDes,&val,sizeof(val));
|
這段代碼在
little-endian
和
big-endian
機器上執行的結果是不一樣的,如果讀數據的時候使用如下代碼:
long val ;
int result = read(fileDes,&val,sizeof(long));
|
如果這兩段代碼分別位于
little-endian
和
big-endian
機器上,那么最終得到的
val
不是
1
,而是
0x01000000
。
解決多字節的讀寫有很多辦法,這里提供兩種。
方法
1
:
寫的代碼
long val = 1;
char buf[4];
buf[0] = 0xff&val;
buf[1] = (0xff00&val)>>8;
buf[2] = (0xff0000&val)>>16;
buf[3] = (0xff000000&val)>>24;
int result = write(fileDes,buf,4);
|
讀的代碼
long val;
char buf[4];
int result = read(fileDes,buf,4);
val = buf[0]|(buf[1]<<8)|(buf[2]<<16)|(buf[3]<<24);
|
3.?
運行時檢查字節順序
bool gIsBigEndian;
void InitializeEndianFlag()
{
Short one = 1;
Char *cp = (char *)&one;
If(*cp == 0)
??? gIsBigEndian = true;
else
??? gIsBigEndian = false;
return ;
}
|
4.?
字節交換對性能的影響
由于字節順序的問題導致在處理的時候需要進行字節交換或類似
2
中方法
1
的處理,這里稱為交換。通常情況下,做字節順序的交換并不影響,因為交換兩個字節或四個字節值只需要很少的
CPU
指令,并且完全可以在寄存器中執行。
但如果有很多數據需要交換,例如:一個
1024*768
位圖的圖像,在這么大的循環中執行是影響性能的。
另外對于
3
的運行時檢查字節序的代碼要查看具體的位置。如果僅僅調用一次或幾次,不會影響性能,如果對于上面的這個循環中調用,對性能的影響是顯著的,這個時候可以使用一個預編譯宏來分別處理。例如:
#ifdef BIG_ENDIAN//big-endian
…
#else//little-endian
…
#endif//BIG_ENDIAN
|
?
三,????????????
字節填充
另一個寫可移植代碼的注意點是結構體的字節對齊和填充。通常,在單個平臺上,如果需要保存一個結構體到文件,那么是作為一個整體寫到文件的,如下:
struct myStruct{
char theChar;
long theLong;
};
struct myStruct foo;
foo.the Char = 1;
foo.theLong = 2;
|
如果我們已經將數據按照
big-endian
進行了交換,然后直接將整個結構體寫到文件中。那么什么樣的數據會被寫到磁盤上呢?
int result = write(fileDes, &foo, sizeof(foo));
|
實際上我們不知道具體寫了什么數據,因為我們還不知道這個代碼在什么平臺上運行;實際上上面的
code
中會將垃圾數據寫到文件里,垃圾數據多少由
foo
分配到的內存決定的。
一種可能我們認為的情況是:
但我們可能得到的這樣的數據:
甚至是:
這里到底發生了什么?
sizeof(foo)
是編譯器和處理器依賴的。
有些處理器不能從某些位置讀或寫多個字節;幾乎所有的都不能從奇數地址來讀數據。通常他們只讀那些是
sizeof
(
value
)倍數的地址;對于四個字節只能讀地址是
4
個字節的倍數,對于
2
個字節的
short
只能讀兩個字節倍數的地址。如果不遵從這個字節對齊的規律,處理器會拋出一個異常并且終止程序,有些系統上會鎖定機器(如果發生在
kernel
中)。
有時,讀沒有對齊的數據需要花費額外的時間。例如:
PowerPC
能夠讀任何偶數地址,但對于那些不能被
4
整除的地址需要耗費額外的總線周期。為了讀一個
long
數值(
value
)在
2
整除而不是
4
整除的地址,它將讀四個字節并包括需要讀的值的上面兩個字節,拋棄
2
個字節,然后讀另外四個包含
value
低
2
個字節的字節,同樣拋棄另外兩個。這與讀
4
個字節對齊的地址相比需要多訪問一次緩存。
為了達到字節對齊的目的,編譯器會插入未命名的填充字節到結構體中。至于插入幾個字節是通過編譯器和
OS
或庫內存分配器一起決定的。
在
Windows VC
編譯器中,可以使用
#pragma
來指定字節對齊的方式。
總而言之,在定義結構的時候要按照字節邊界對齊來定義,一般按照
4
個字節,如果不夠就需要增加填充字段。
另外對于結構體寫文件或輸出到網絡上,最好的辦法是按照成員來逐個寫入或發送,這可以避免將垃圾數據存放到文件中或傳輸到網絡上。
?
四,????????????
其他
下面是幾個筆者在實際編寫代碼中發生過的錯誤,這里與大家一道分析一下。
1.????????
示例
1
:
for(int i = 0;i<1000;i++)
{
?? ….
}
...
for(int i = 0;i<1000;i++)
{
...
}
|
上面這段代碼是很普通的
C++
代碼,但這段代碼不一定可以在所有的編譯器中都能編譯通過。主要的原因在于變量
i
的聲明。
C++
標準說:在
for
循環內部聲明的變量在
for
結束的時候無效,因此可以連續使用再次在
for
循環中使用該記數器變量。但很不幸的是很多編譯器都提供編譯選項來讓你覺得變量是否在
for
循環以后仍然有效。
VC
中默認編譯選項
/Ze
用來指定
for
循環變量的局部性,但并非所有的編譯器都是將這個選項作為默認編譯參數;所以為了能讓你的代碼可以在任意平臺編譯通過,使用
C
風格的會有保證一點;如下:
int i = 0;
for(i = 0;i<1000;i++)
{
?? ….
}
...
for(i = 0;i<1000;i++)
{
...
}
|
?
2.????????
示例
2
:
int
型變量的使用
Int
型變量是一個奇怪的東西,它在
16
位機器上是
2
個字節,在
32
位機上是
4
個字節;將來可能在
64
位機上是
8
個字節。所以如果你的代碼中有對
int
的使用,而你想代碼可以在很多平臺上運行,那么一定要注意了。看一下下面的情況:
for(int i = 0;i<65540;i++)
{
?? ….
}
|
這個代碼可能在不同的平臺上得到不同的結果。如果這個代碼是在
16
位機器上運行,那么得到的結果與
32
位機器上可能不同。
同樣在使用
int
型變量寫文件和輸出到網絡時都要小心這個問題。最好的辦法是,在這些情況下不要使用
int
型變量;
int
型變量僅僅在程序內部使用。
3.????????
關于
Bit field
的問題
在
C
語法中有
bit field
的語法,可以根據需要來定義一個符號具體占用的
bit
數,例如:
typedef struct tagTest { ???char a:4; ?? char b:2; ?? char c:2; }TagTest,*PTagTest;
|
實際上
tagTest
的字節數是
1
個字節,成員
a
占用
4
位,
b
和各占用兩位。這樣的好處是可以針對每個成員賦值而設置指定的位的值,例如:
tagTest
myTest; myTest.a = 10; myTest.b = 2; myTest.c = 1;
|
假如你在
Windows
上是使用
VC
來編譯連接上面的程序,不管如何處理,你不會發生任何問題。但現在我們假設將
myTest
放入緩沖區中,然后在
MAC
機器上取出來,那么會發生什么來?看代碼:
Windows:
char buf[10];
buf[0] = myTest;
buf[2]=...
int result = send(fd,buf,10,..);
?
MAC:
char buf[10];
int ret = 0;
int result = recv(fd,buf,10,..);
PTagTest pTest = (PTagTest)&buf[0];
?
if(pTest->a == 10)
?? ret = 1;
else
??? ret = 0;
...
|
那么
ret
的值是什么呢?我們期望是
1
但,結果不是
1
。如果你通過調試器來觀察一下
pTest
各成員的值你發現:
pTest->a = 6; pTest->b =2 ; pTest->c =2;
細心的讀者可能發現這里的問題所在,原因在于不同的編譯器對
bit field
進行了不同的處理。在
Windows
平臺上,
c
被放在字節的最高兩位,而
a
被放在字節的最低
4
位,在
MAC
上正好相反。但一定要注意,這是編譯器行為,而不是數據在傳輸過程中發生了字節的位交換。在
Windows
發送到網絡的時候,
buf[0]
的內容二進制表示為:
在
MAC
上
recv
之后,
buf[0]
的內容仍然與上面的相同。
為了避免這個問題,請不要在寫文件或網絡輸出的時候使用
BIT FILED
語法,如果一定要使用請注意編譯器對位處理的區別。
n????????
五
小結
其實實際工作中,大家認為自己的代碼都不需要在多個平臺上運行,而認為跨平臺編碼與自己無關;其實不然,好的編碼習慣是慢慢養成的,如果大家都知道這些跨平臺編碼的細節,在開始寫代碼的時候就開始避免這樣的問題,一旦有一天我們的代碼需要跨平臺運行或一點我們要寫跨平臺代碼時,我們就不會無從下手,而是順其自然,因為我們已經具備了這樣的習慣。
當然這里的介紹只是一個開始,跨平臺編碼涉及的問題還很多,由于筆者經驗的限制不能一一描述。
?
本文參考:
http://www.goingware.com/tips/getting-started/