字节序、位序
字節序
??? 字節序,又稱端序、尾序,英文單詞為Endian,該單詞來源于于喬納森·斯威夫特的小說《格列佛游記》,小說中的小人國因為吃雞蛋的問題而內戰,戰爭開始是由于以下的原因:我們大家都認為,吃雞蛋前,原始的方法是打破雞蛋較大的一端??墒钱斀窕实鄣淖娓感r候吃雞蛋,一次按古法打雞蛋時碰巧將一個手指弄破了,因此他的父親,當時的皇帝,就下了一道敕令,命令全體臣民吃雞蛋時打破雞蛋較小的一端,違令者重罰。老百姓們對這項命令極為反感。歷史告訴我們,由此曾發生過六次叛亂,其中一個皇帝送了命,另一個丟了王位…關于這一爭端,曾出版過幾百本大部著作,不過大端派的書一直是受禁的,法律也規定該派的任何人不得做官。?1980年,Danny Cohen在其著名的論文"On Holy Wars and a Plea for Peace"中,為平息一場關于字節該以什么樣的順序傳送的爭論,而引用了該詞。
?????????在計算機科學領域中,字節序是指存放多字節數據的字節(byte)的順序,典型的情況是整數在內存中的存放方式和網絡傳輸的傳輸順序。有時候也可以用指位序(bit)。為了更好地理解,先看下面這段小程序,這個程序是把一個包含4位數字的字符串轉換為16進制整數來存儲,16進制整數的每一個字節存儲一位數字字符。比如:”1234”,轉換成16進制整數0x01020304。
程序1清單:
#include?<stdio.h>
#include?<conio.h>
?
int?main( )
{
?????char?input[4] = {0};
?????int?integer???= 0;
?????int?i;
?????printf("/r/n請輸入一個位數,每一位的范圍是從到0到9/r/n");
?????for(i = 0; i < 4; i++)
?????{
?????????input[i] = getch();
?????????if(input[i] >?'9'?|| input[i] <?'0')
?????????{
??????????????printf("input error!/r/n");
??????????????return?1;
?????????}
?????????putch(input[i]);
?????}
????getch();
?????putch('/n');
?
?????for(i = 0; i < 4; i++)
?????{
?????????input[i] = input[i] -?'0';
?????}
?
?????memcpy((void*)&integer, (void*)input, 4);
?
?????printf("轉換后的進制數是:0x%08x/r/n", integer);
?
?????getch();
?
?????return?0;
}
?
現在來分析一下這段代碼
首先,定義了一個字符數組input,用來接收用戶輸入的4個數字字符;
第二,把4個字符數字轉換成對應的數字;
第三,把轉換的數字復制到整型變量integer中;
最后在屏幕上打印。
如果在PPC或者ARM的機器上編譯運行這個程序,那么會在屏幕上打印出結果:0x01020304,這與我們的預期一致;但是在X86的機器上則打印出的結果是:0x04030201。這個令人驚訝的結果正是字節序問題引起。下面來詳細談談這個問題。
?
????從計算機誕生之后,就有幾種不同的字節序,典型的是大端序(big endian)和小端序(little endian),當然還有不常見的混合序(middle endian)。這些用來都是描述多字節數據在內存中的存放方式的。以上面的16進制數0x01020304為例,在計算機中需要用4個字節來保存它,’01’, ’02’,’03’,’04’各占一個字節。按照人類的計數習慣,最左邊的’01’,稱之為最高有效位(MSB,Most Significant Byte,它具有最高權重);最右邊的’04’稱之為最低有效位(LSB, Least Significant Byte,他具有最低權重);在計算機中需要用4個字節來保存它,其中’01’, ’02’,’03’,’04’各占一個字節。大端序的計算機保存這個數值時,按照從低地址到高地址的順序分別保存MSB到LSB四個字節,0x01020304的存儲情況如下圖所示:
?
而小端序的計算機則以相反的順序來保存它,如下圖所示:
?
?
可以看到,在小端序的計算機中,0x01020304的保存順序恰好與上面的程序1中相反,這就是最后輸出結果為0x04030201的原因;大端序的計算機中兩者保存順序一致,所以打印正確。
?
??? 我們可以在VC集成環境中來驗證上面的分析。在VS2005中調試下面的小程序,在return語句處設置斷點,斷住后打開內存窗口查看&i處的內容。可以直觀的看到在x86(小端序)的機器上整數的存放方式。
程序2清單:
#include?<stdio.h>
int?main()
{
?????int?i = 0x01020304;
?????printf("i = %#x/r/n",i);
?????return?0;
}
?
?
?
?
?
再看看如何才能讓程序1在大端序和小端序的機器上都能正確執行呢?一個辦法就是利用預編譯宏,針對不同的機器定義定義不同的數據結構。下面是一個例子:
程序3清單:
#include?<stdio.h>
#include?<conio.h>
?
typedef?union
{
?????struct{
#ifdef?BIG_ENDIAN
?????char msb;
?????char midb1;
?????char midb2;
?????char lsb;
#else
?????char?lsb;
?????char?midb2;
?????char?midb1;
?????char?msb;
#endif
?????} bytes;
?????int??var;
} INTEGER;
?
int?main()
{
?????int??i??????????= 0;
?????char?input[5]???= {0};
?????INTEGER integer = {0};
?
?????printf("/r/n請輸入一個位數,每一位的范圍是從到到/r/n");
?????for(i = 0; i < 4; i++)
?????{
?????????input[i] = getch();
?????????if(input[i] >?'9'?|| input[i] <?'0')
?????????{
??????????????printf("input error!/r/n");
??????????????return?1;
?????????}
?????????putch(input[i]);
?????}
????getch();
?????putch('/n');
?
?????integer.bytes.msb???= input[0] -?'0';
?????integer.bytes.midb1 = input[1] -?'0';
?????integer.bytes.midb2 = input[2] -?'0';
?????integer.bytes.lsb???= input[3] -?'0';
?
?????printf("轉換后的進制數是:0x%08x/r/n", integer.var);
?
?????getch();
?
?????return?0;
}
可以看到,這段代碼定義了兩套數據結構,通過BIG_ENDIAN這個宏定義來決定使用哪一套數據結構。這是個笨拙卻有效的方法。下面這個例子則漂亮一些:
程序4清單
#include?<stdio.h>
#include?<conio.h>
#include?<memory.h>
#include?<winsock2.h>
?
int?main( )
{
?????char?input[4] = {0};
?????int?integer???= 0;
?????int?i;
?????printf("/r/n請輸入一個位數,每一位的范圍是從到到/r/n");
?????for(i = 0; i < 4; i++)
?????{
?????????input[i] = getch();
?????????if(input[i] >?'9'?|| input[i] <?'0')
?????????{
??????????????printf("input error!/r/n");
??????????????return?1;
?????????}
?????????putch(input[i]);
?????}
????getch();
?????putch('/n');
?
?????for(i = 0; i < 4; i++)
?????{
?????????input[i] = input[i] -?'0';
?????}
?
?????memcpy((void*)&integer, (void*)input, 4);
?
?????integer = ntohl(integer);
?
?????printf("轉換后的進制數是:0x%08x/r/n", integer);
?
?????getch();
?
?????return?0;
}
這個程序利用了大端序與人類書寫習慣一致的特點,通過ntohl函數將整數進行轉換。這個函數的功能是將網絡序轉換成主機序,在大端機器上它什么也不做,在小端機器上它會將輸入參數的值轉換成小端序的值。在windows環境下鏈接時別忘了將Ws2_32.lib庫添加進來。
?
主機序和網絡序
主機序就是指主機的端序。
網絡字節序(網絡序)指多字節數據在網絡傳輸中的順序,TCP協議規定網絡序是大端序,即高字節先發送。因此大端序的機器接受到的數據可直接使用,小端序機器則需要轉換后使用。BSD socket API中定義了一組轉換函數,用于16和32bit整數在網絡序和本機字節序之間的轉換。htonl,htons用于本機序轉換到網絡序;ntohl,ntohs用于網絡序轉換到本機序。一般來說,為了保證程序的可移植性,編寫代碼時,發送的數據需要使用htonl、htons轉換,接收到的數據要使用ntohl、ntohs轉換。
注意:不存在對單字節整數進行轉換的函數”ntohc”和”htonc”!
位序
?????????位序,一般用于描述串行設備的傳輸順序。一般說來大部分硬件都是采用小端序(先傳低位),因此,對于一個字節數據,大部分機器上收發的順序都一樣,不會有問題,這就是為什么沒有針對單字節數據的API接口”ntohc”和”htonc”。當然,也有例外,比如-I2C協議就是采用了大端序。這些細節只有在網絡協議的數據鏈路層底端才會碰到,對一般的程序員來說很少涉及。
?????????但是在C語言中存在一種特殊的數據結構:位域。它的存在,使得C程序員能方便地進行位操作(比如在網絡協議中經常出現1bit或者多bit的標示位,它們不是一個完整的字節)。但同時也引起一些難以察覺的問題,這些問題的根源仍然是前面提到的端序。
?
?????????與字節序一樣,一個字節中的8個bit順序在不同端序的機器上并不相同。大端機器上從低地址到高地址順尋分別是msb->lsb,如下圖:
?
?
小端序的機器上則正好相反?
現代計算機的最小存儲單位是BYTE,無法對bit尋址,因此我們無法直接觀察每個字節內部bit的順序。但是我們仍然可以通過位域來間接觀察字節內部bit順序,以印證上面的說法。
在C語言中,位域與結構體類似,其語法規定:先聲明的成員位于低地址,后聲明的成員位于高地址。那么下面的位域中:
typedef?struct?OneByte
{
?????bt0 : 1;
?????bt1 : 1;
?????bt2 : 1;
?????bt3 : 1;
?????bt4 : 1;
?????bt5 : 1;
?????bt6 : 1;
?????bt7 : 1;
}
成員bt0就位于一個字節中最低地址bit0處,成員bt7就位于一個字節的最地址bit7處。
?
我們看看下面的程序。
#include?<stdio.h>
?
typedef?struct?OneByte
{
?????char?bt0 : 1;
?????char?bt1 : 1;
?????char?bt2 : 1;
?????char?bt3 : 1;
?????char?bt4 : 1;
?????char?bt5 : 1;
?????char?bt6 : 1;
?????char?bt7 : 1;
} ONE_BYTE;
?
int?main()
{
?????ONE_BYTE onebyte = {0};
????
?????onebyte.bt7 = 1;
?
?????printf("onebyte = %#x/r/n", *((unsigned?char?*)&onebyte));
?
?????return?0;
}
?
當bt7賦值為1后,onebyte在內存中是這個樣子的:
?
而在VC2005中編譯運行的結果如下:
?
0x80轉換成二進制是1000 0000。由于在X86(小端序)中,高地址bit7是msb,因此onebyte的值是0x80了;這就證實了前面的說法。?相應的如果是在大端序計算機中,bit7是lsb,則onebyte的值是0x01。
?
?
?
未完待續
總結
- 上一篇: 科创板新股中签一手是多少股?科创板新股申
- 下一篇: linux+qt+定时精度,Qt QTi