談一談字符編碼的事
字符編碼是程序員最頭疼的問題,有一句話可以形容字符編碼的重要性。“大家都統一用UTF,不然最後怎麼死的都不知道”。因爲文字是用戶日常交流的基礎,而字符編碼是文字在計算機系統中的表示,如果在一個系統中字符編碼都沒有確定,那麼最後很容易出現亂碼問題。對於亂碼問題,當涉及到的模塊增多,系統間的交互增多,解決起來就變得異常困難。字符編碼無小事,不要認爲你寫的程序很小,不涉及到多個模塊間的交互,就可以避免字符編碼問題。這個想法是完全的錯誤。編碼問題非常基礎,如一個字符串拷貝函數就會涉及,估計沒有人會不用吧。那麼當你使用字符處理函數時,有考慮過內存裏面的0101到底採用是什麼編碼嗎?經過操作的字符串最後還能否會被轉化爲可讀的文字。下面我們談一談字符編碼的問題。
1. 什麼是字符編碼
字符編碼,指對人們使用的文字中的每一個符號,用二進制0和1組成的串進行表示。不同的符號對應的01串不同,每個符號對應的01串就是該符號的編碼。
2. 字符編碼中的常見概念
A. ASCII碼
ASCII(American Standard Code for Information Interchange,美國信息互換標準代碼,ASCⅡ)是基於拉丁字母的一套電腦編碼。它主要用於顯示現代英語和其他西歐語言,它採用單字節編碼。
ASCII共定義了128個字符,其中33個字符無法顯示,在33個字符之外的是95個可顯示的字符,包含用鍵盤敲下空白鍵所產生的空白字符也是1個可顯示字符(顯示爲空白)。可以在這個網站查詢到ASCII的對照表http://ascii.911cha.com/。
補充:ASCII第一次以規範標準的形態發表是在1967年,最後一次更新則是在1986年,至今爲止共定義了128個字符,其中33個字符無法顯示(這是以現今操作系統爲依據,但在DOS模式下可顯示出一些諸如笑臉、撲克牌花式等8-bit符號),且這33個字符多數都是已廢的控制字符,控制字符的用途主要是用來操控已經處理過的文字。
B. GB2312
ASCII碼是單字節編碼方式,對於漢字有10萬多個字符的語言,單字節編碼僅能表示256個字符。
GB 2312或GB 2312-80是一個簡體中文字符集的中國國家標準,全稱爲《信息交換用漢字編碼字符集·基本集》,又稱爲GB0,由中國國家標準總局發佈,1981年5月1日實施。GB2312編碼通行於中國大陸。
具體的編碼規則就不研究了,開發時不用去了解具體的編碼規則。我們只要知道每個漢字及符號以兩個字節來表示。同時,GB 2312標準共收錄6763個漢字,其中一級漢字3755個,二級漢字3008個,可見生僻字不能顯示。
補充:GB 2312還收錄了包括拉丁字母、希臘字母、日文平假名及片假名字母、俄語西裏爾字母在內的682個全角字符。GB 2312的出現,基本滿足了漢字的計算機處理需要,它所收錄的漢字已經覆蓋中國大陸99.75%的使用頻率。新加坡等地也採用此編碼。
C. ANSI編碼
因爲ASCII碼的侷限性,不同的國家和地區制定了不同的擴展標準,由此產生了 GB2312, BIG5, JIS等各自的編碼標準。在簡體中文系統下,ANSI編碼代表 GB2312編碼,在日文操作系統下,ANSI 編碼代表 JIS 編碼。 不同 ANSI 編碼之間互不兼容,當信息在國際間交流時,無法將屬於兩種語言的文字,存儲在同一段 ANSI編碼的文本中。
D. Unicode編碼
Unicode也是一種字符編碼方法,不過它是由國際組織設計,可以容納全世界所有語言文字的編碼方案。Unicode的學名是"Universal Multiple-Octet Coded Character Set",簡稱爲UCS。UCS可以看作是"Unicode Character Set"的縮寫。
UCS有兩種格式:UCS-2和UCS-4。顧名思義,UCS-2就是用兩個字節編碼,UCS-4就是用4個字節(實際上只用了31位,最高位必須爲0)編碼。
UCS規定了怎麼用多個字節表示各種文字。怎樣存儲、傳輸這些編碼,是由UTF(UCS Transformation Format)規範規定的,常見的UTF規範包括UTF-8、UTF-7、UTF-16。
E. UTF編碼
UTF-8就是以8位爲單元對UCS進行編碼,它是一種變長的編碼方式。它可以使用1-4個字節表示一個符號,根據不同的符號而變化字節長度。從UCS-2到UTF-8的編碼方式如下:
例如“漢”字的Unicode編碼是6C49。6C49在0800-FFFF之間,所以肯定要用3字節模板了:1110xxxx 10xxxxxx 10xxxxxx。將6C49寫成二進制是:0110 110001 001001, 用這個比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。
UTF-16以16位爲單元對UCS進行編碼。對於小於0x10000的UCS碼,UTF-16編碼就等於UCS碼對應的16位無符號整數。對於不小於0x10000的UCS碼,定義了一個算法。不過由於實際使用的UCS2,或者UCS4的BMP必然小於0x10000,所以就目前而言,可以認爲UTF-16和UCS-2基本相同。UCS-2只是一個編碼方案,UTF-16卻要用於實際的傳輸,所以就不得不考慮字節序的問題。
F. 字節序(Little endian和Big endian)和BOM
UTF-8以字節爲編碼單元,沒有字節序的問題。UTF-16以兩個字節爲編碼單元。
UTF-16使用的兩個字節存在一個順序的問題。比如“奎”的Unicode編碼是594E,“乙”的Unicode編碼是4E59。那麼當我們收到UTF-16字節流“594E”,那麼這是“奎”還是“乙”?
字節序就是規定UTF-16所用兩個字節的順序。以漢字”嚴“爲例,Unicode碼是4E25,需要用兩個字節存儲,一個字節是4E,另一個字節是25。存儲的時候,4E在前,25在後,就是Big endian方式;25在前,4E在後,就是Little endian方式。
在UCS編碼中有一個叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的編碼是FEFF。而FEFF在UCS中是不存在的字符,所以不應該出現在實際傳輸中。UCS規範建議我們在傳輸字節流前,先傳輸字符"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編碼了。
Windows就是使用BOM來標記文本文件的編碼方式的。Windows中的Unicode字符一般指UCS2的UTF16-LE編碼。
3. 編碼間如何轉化
A. ANSI編碼和UCS-2 LE編碼
我們以漢字“你好”爲例進行說明,先看一下“你好”的各種編碼:
漢字 |
ANSI |
UTF-8 |
UCS-2 BE |
UCS-2 LE |
你好 |
C4 E3 BA C3 |
EF BB BF(BOM區分編碼) E4 BD A0 E5 A5 BD |
FE FF(BOM表示字節序) 4F 60 59 7D |
FF FE(BOM表示字節序) 60 4F 7D 59 |
輸出編碼程序:
1) ANSI編碼
char* pchar = "你好";
int len = strlen(pchar);
for (int i=0; i<len; i++ )
{
char each = pchar[i];
printf("Hex: %x\r\n",each );
}
printf("===========================\r\n");
2) UCS-2 LE
wchar_t* pwchar = L"你好";
int wlen = wcslen( pwchar );
int bytelen = wlen * sizeof( wchar_t );
char* point = (char*) pwchar;//強制轉化爲char*,按照一個字節一個字節的方式輸出內存中的內容
for (int i=0; i<bytelen; i++ )
{
char each = point[i];
printf("Hex: %x\r\n",each );
}
printf("===========================\r\n");
B. ANSI與UCS-2 LE之間的轉換
1) Ansi轉化爲Unicode
MultiByteToWideChar返回的是字符數,需要乘以sizeof(wchar_t)轉化爲內存大小。返回的字符數包含了字符串的結尾字符’\0’需要的一個字符。
所以輸出結果:
2) Unicode轉化爲Ansi
WideCharToMultiByte返回的是需要的字節數,可以直接分配內存,返回的字節數包含目標ANSI以’\0’爲結尾的字節數。
所以輸出結果:
C. UTF-8與UCS-2 LE之間的轉換
1) Unicode轉化爲UTF-8
WideCharToMultiByte返回的是需要的字節數,可以直接分配內存,返回的字節數包含目標UTF-8以’\0’爲結尾的字節數。
所以輸出結果:
UTF-8’\0’以單字節表示
4. 不同字符編碼下字符串如何結尾?
字符串都以\0結束,但是不同編碼存儲\0所用的字節數不同。通過上面的實驗可以看到,ANSI和UTF-8採用單字節存儲\0,Unicode採用雙字節存儲\0。
5. URLENCODE是字符編碼嗎?
URLENCODE與前面的ANSI,Unicode,編碼不同。URLENCODE是對字符經過ANSI、Unicode編碼後的結果,再次轉化便於URL傳輸。
比如:
“中文” -> GB2312編碼爲D6D0CEC4,經過Encode -> %D6%D0%CE%C4
“中文”-> UTF-8編碼爲E4B8ADE69687,經過Encode -> %E4%B8%AD%E6%96%87