在計算機中是以字節為單位,每個地址對應一個字節,一個字節8bit。在C中,除了8bit的char以外,還有16bit的short,32位的int,64位long,當然具體要由編譯器決定,可以通過sizeof來獲取不同類型在內存中占用的字節數。在計算機系統中,當物理單位的長度大於1個字節時,就要區分字節順序。常見的字節順序有兩種:Big Endian(High-byte first)和Litter Endian(Low-byte first),當然還有其他字節順序,但不常見,例如Middle Endian。
一、最高有效位、最低有效位
要理解Big Endian和Little Endian,首先要搞清楚MSB和LSB。
1、MSB(Most Significant Bit)最高有效位
在一個n位二進制數字中n-1位,也就是最左邊的位。
2、LSB(Least Significant Bit)最低有效位
指最右邊的位。
例如:一個int類型的整型123456789
二進制表達方式:0000 0111 0101 1011 1100 1101 0001 0101(從右向左,每4bit對齊,最左邊(高位)不夠用0補齊)
十六進制表達方式:0 7 5 B C D 1 5
按照上述關於MSB和LSB的意思,在二進制表達方式中,bit從0開始,從右向左,bit0為最低有效位,而bit23為最高有效位。而我們一般稱左邊的0x07為高位字節,0x15為低位字節。
再通俗一點解釋就是:8421碼的,8這端為高位,1這端為低位,相應的字節則分別稱為高位字節和低位字節。
二、內存地址
在內存中,多字節對象都是被存儲為連續的字節序列。例如在C語言中,一個類型為int的變量n,如果其存儲的首個字節的地址為0x1000,那么剩余3個字節的地址將存儲在0x1001~0x1003。總之,不管具體字節順序是以什么方式排列,內存地址的分配一般是從小到大的增長。我們常把0x1000稱為低地址端,把0x1003稱為高地址端。
三、大端和小端
搞清楚MSB、LSB、高位字節、低位字節、內存地址之后,再理解大端和小端,就相當容易了,先看看概念:
小端Little Endian:低字節存放在低地址,低位字節排放在內存的低地址端,高位字節排放在內存的高地址端。
大端Big Endian:高字節存放在低地址,即高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。
以二節中的例子int類型整數123456789為例:
小端在內存中排列:0x15 0xCD 0x5B 0x07 (低位在前)
大端在內存中排列:0x07 0x5B 0xCD 0x15 (高位在前)
從例子中可以看出小端比較符合人的思維,而大端則看上去非常直觀。
注:
1、例子中是假設編譯器支持int為32位的前提下,如果是16位,那大端的排列則為:0xCD 0x15 0x07 0x5B。
2、大小端一般是由CPU架構決定,常見的Intel、AMD的CPU使用的是小端字節序,而PowerPC使用的是大端字節序,有些ARM處理器還可以選擇用大端還是小端模式,具體請自行查閱。
3、c#中,字節序跟編譯平台所在的CPU相關,例如在Intel x86 CPU架構的windows平台中,c#采用的小端序。而Java由於其JVM屏蔽了不同CPU架構導致的字節序差異,所以默認采用大端字節序。所以,大小端模式是由CPU決定,而編譯器又可能會改變這種模式。
四、網絡字節序和主機字節序
網絡字節序(Network Order):TCP/IP各層協議將字節序定義為Big Endian,因此TCP/IP協議中使用的字節序通常稱之為網絡字節序。
主機字節序(Host Order):整數在內存中保存的順序,它遵循Little Endian規則(不一定,要看主機的CPU架構)。所以當兩台主機之間要通過TCP/IP協議進行通信的時候就需要調用相應的函數進行主機序列(Little Endian)和網絡序(Big Endian)的轉換。
如果是做跨平台開發時,雙方需要協商好字節序,然后根據程序運行的環境,確定是否需要字節序轉換。
例如約定的通訊字節序位Big Endian,默認的windows采用的Little Endian,那收到數據后就需要做轉換操作。
五、C#位操作符
這里簡單記錄一下C#的位操作符,方便以后自己查閱,也方便理解后面的講解。
1、按位與&
1&0為0;0&0為0;1&1為1。
2、按位或|
1|0為1;0|0為0;1|1為1。
3、按位取反~
~1為0;~0為1。
4、按位異或^
1^1為0;0^0為0;1^0為1。相等得0,相異等1。
5、左移<<
位左移運算,將整個數向左移若干位,左移后空出的部分用0補齊。
6、右移>>
位右移運算,將整個數向右移若干位,右移后空出的部分用0補齊。
六、C#中關於大端和小端的轉換
1、重復輪子
1 namespace Framework.NetPackage.Common 2 { 3 /// <summary>
4 /// 字節序轉換輔助類 5 /// </summary>
6 public static class Endian 7 { 8 public static short SwapInt16(this short n) 9 { 10 return (short)(((n & 0xff) << 8) | ((n >> 8) & 0xff)); 11 } 12
13 public static ushort SwapUInt16(this ushort n) 14 { 15 return (ushort)(((n & 0xff) << 8) | ((n >> 8) & 0xff)); 16 } 17
18 public static int SwapInt32(this int n) 19 { 20 return (int)(((SwapInt16((short)n) & 0xffff) << 0x10) |
21 (SwapInt16((short)(n >> 0x10)) & 0xffff)); 22 } 23
24 public static uint SwapUInt32(this uint n) 25 { 26 return (uint)(((SwapUInt16((ushort)n) & 0xffff) << 0x10) |
27 (SwapUInt16((ushort)(n >> 0x10)) & 0xffff)); 28 } 29
30 public static long SwapInt64(this long n) 31 { 32 return (long)(((SwapInt32((int)n) & 0xffffffffL) << 0x20) |
33 (SwapInt32((int)(n >> 0x20)) & 0xffffffffL)); 34 } 35
36 public static ulong SwapUInt64(this ulong n) 37 { 38 return (ulong)(((SwapUInt32((uint)n) & 0xffffffffL) << 0x20) |
39 (SwapUInt32((uint)(n >> 0x20)) & 0xffffffffL)); 40 } 41 } 42 }
2、BCL庫支持的函數
System.Net.IPAddress.HostToNetworkOrder、System.Net.IPAddress.NetworkToHostOrder,這兩個函數的內部實現和上面重復輪子原理一模一樣。
七、關於負數
在計算機中,負數以其絕對值的補碼形式表示,不明白可以查閱九中貼出的相關資源。關於負數的字節序跟一般整數的字節序處理沒有任何區別。
八、關於漢字編碼以及與字節序的關系
1、對於gb2312、gbk、gb18030、big5,其編碼某個漢字產生的字節順序,由其編碼方案本身決定,不受CPU字節序的影響。其實這幾種編碼的字節序和大端模式的順序是一致的。
在使用GB2312的程序通常采用EUC儲存方法,以便兼容於ASCII。瀏覽器編碼表上的“GB2312”,通常都是指“EUC-CN”表示法。
每個漢字及符號以兩個字節來表示。第一個字節稱為“高位字節”,第二個字節稱為“低位字節”。
“高位字節”使用了0xA1-0xF7(把01-87區的區號加上0xA0),“低位字節”使用了0xA1-0xFE(把01-94加上0xA0)。
由於一級漢字從16區起始,漢字區的“高位字節”的范圍是0xB0-0xF7,“低位字節”的范圍是0xA1-0xFE,占用的碼位是72*94=6768。其中有5個空位是D7FA-D7FE。
例如“啊”字在大多數程序中,會以兩個字節,0xB0(第一個字節)0xA1(第二個字節)儲存。(與區位碼對比:0xB0=0xA0+16,0xA1=0xA0+1)。
2、UTF-8
UTF-8和gb系列編碼一樣,其編碼某個漢字產生的字節順序,由其編碼方案決定,不受CPU字節序的影響。無論一個漢字有多少個字節,它的字節序與編碼順序保持一致。
例如漢字”嚴”利用utf8編碼過程:
(1)、已知“嚴”的unicode編碼是4E25(100111000100101),根據utf8規則可以得知其utf8編碼需要三個字節。
即格式是“1110xxxx 10xxxxxx 10xxxxxx”
第一個字節前三位表示了字符“嚴”被編碼成utf8后的編碼長度,有多長,則從左開始填多少個1,如果只有1個字節,則第一個位為0。
對於編碼后大於1個字節的符號,第一個字節的第四位為0,其他字節前兩位均要求為10。
(2)、從”嚴“的最后一個二進制位開始,依次從后向前填入格式中的x,多出的位補0。這樣就得到了“嚴”的utf8編碼為“11100100 10111000 10100101”,轉換成十六進制就是E4B8A5。
編碼示例過程參考的原文:http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
從上述過程可以看到,utf8的字節序已經由其編碼方案決定,不受CPU字節序影響。
3、Unicode
Unicode只是一個符號集,它只規定了符號的二進制代碼,卻沒有規定這個二進制代碼應該如何存儲。所以他沒有要求如何存儲編碼后的字節,也就受CPU字節序的影響。
Unicode的具體實現包括UTF-16、UTF-32(當然也包括UTF-8,但由於其編碼方式和編碼后的字節序與其他Unicode編碼實現有較大區別,所以單獨拿出來講解的)。
4、總結
(1)、網絡通訊
在實際的網絡通訊中,網絡協議例如TCP是規定網絡字節序(Network Order)是大端。而針對漢字具體使用什么編碼,通訊雙方要么提前約定好,要么就需要在數據包中標識好漢字具體使用的編碼。
如果在網絡通訊中,涉及例如UTF16這樣區分大小端的編碼,除非按網絡協議要求采用大端模式是,否則也要事先約定好,或者在數據包中標識好使用的字節序模式。
(2)、文件
文件的也會存儲漢字,當然也要進行編碼。如果采用UTF-16這樣的有字節序模式區分的編碼,編碼規則要求可以在文件頭部的BOM(Byte Order Mark)來標記。如果沒有標記,除非事先知道字節序的模式,否則只能大小端都試一遍。
Unicode規范中推薦的標記字節順序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。BOM是一個有點小聰明的想法:
在Unicode編碼中有一個叫做”ZERO WIDTH NO-BREAK SPACE”的字符,它的編碼是FEFF。而FEFF在Unicode中是不存在的字符,所以不應該出現在實際傳輸中。UCS(Unicode的學名)規范建議我們在傳輸字節流前,先傳輸字符“ZERO WIDTH NO-BREAK SPACE”。
這樣如果接收者收到FEFF,就表明這個字節流是Big-Endian的;如果收到FFFE,就表明這個字節流是Little-Endian的。因此字符“ZERO WIDTH NO-BREAK SPACE”又被稱作BOM。
UTF-8不需要BOM來表明字節順序,但可以用BOM來表明編碼方式。字符“ZERO WIDTH NO-BREAK SPACE”的UTF-8編碼是EF BB BF。所以如果接收者收到以EF BB BF開頭的字節流,就知道這是UTF-8編碼了。