以下內容轉自博客:http://blog.chinaunix.net/uid-22670933-id-1771613.html。 一、字元編碼是怎麼回事 0. 概念 位元組是電腦的最基本存儲單位,一個位元組包括8個位. 字元是一種文字的基本單位,比如'A' 是一個字元,'漢' 也是一個字元. 1. 計 ...
以下內容轉自博客:http://blog.chinaunix.net/uid-22670933-id-1771613.html。
一、字元編碼是怎麼回事
0. 概念
位元組是電腦的最基本存儲單位,一個位元組包括8個位.
字元是一種文字的基本單位,比如'A' 是一個字元,'漢' 也是一個字元.
1. 電腦被髮明之後,程式員們編寫了很多複雜的計算讓電腦運行.
但是一個問題是,電腦如何把辛苦計算的結果告知程式員? 假設電腦把計算結果放在某個寄存器,內容是 1010010
總不能讓程式員去檢測每個引腳的電位吧? 還是得有個顯示器.
顯示器是依靠點陣來顯示圖像的. CPU必須告訴顯示器,當CPU 把一個位元組的數據比如 00101010 放入顯示器寄存器的時候,顯示器要顯示怎樣的一個點陣(圖像),這個圖像就是我們人類可以看得懂的字元. 這樣問題就解決了,比如當CPU把00110001放入顯示器寄存器時,顯示器就顯示(控制點陣畫出一個圖像-字體)字元 "1", 這是一個查表的過程,記憶體中的值(內碼)和字元是一一對應的. 問題是這個對應關係是可以自由確定的,我可以指定顯示器把 00110001(內碼) 顯示為字元 "1",也可以指定顯示為字元"2". 這樣當然會引起混亂,同一個內碼被映射為不同的字元,不利於人們的交流.
美國國家標準學會(ANSI)決定著手解決這個問題, 英語有一個很小的字元集,26個字母再加上一些控制字元和標點符號,7位2進位值就足以表示所有的變化.於是ANSI公佈了一個標準的對應關係,以位元組為單位. 當內碼為 0110001 時,大家都公認它代表字元 "1",在所有顯示器都顯示為同一個字元. 這樣大家就可以按照同一個標準相互交換數據而不會引起誤解. 這個表就是一個包含了128項的對應關係, 叫做 "ASCII", 美國信息交換標準代碼.
2.對於中國這樣不使用ABC字元的國家來說,如何顯示自己的文字是一個大問題.
我們可以制定一個內碼表,指定一個內碼對應一個漢字. (由於中文的字元非常多,所以一個位元組是不夠的,至少也要有2個位元組存儲一個內碼.) 這是很容易的,只要國家公佈一個標準的內碼字元對應表,大家都遵照這個表就可以了.但是還是有一些問題要註意:
(1). 即使在中國,電腦還是得能顯示英文吧?
而英文的內碼已經有 ASCII 標準在先,並且已經有無數的程式已經在這個標準上運行了很多年,成為不可或缺的部分. 所以我們新制定的內碼表必須和 ASCII 相容.
(2) 很多C語言的庫函數是以內碼0作為字元串結束標誌的,為了相容那些以前就已經編寫好,並且運行良好的程式,我們指定的內碼中不能含有值為0的位元組.
巧合的是,所有的ASCII內碼的最高位都為0. 那麼我們只要讓第一個和第二個位元組(一個漢字占用2個位元組)的最高位都為1,這樣既和ASCII內碼區分開來,又不會出現0.符合這個規則的內碼(2位元組長)理論上一共可以標識 127 * 127 = 16129個字元(實際上只用了6000多個碼位,保留了一些,不過也已經夠用了,常用的中文字元只有4000多個).
我們國家公佈的這個內碼標準表就是GB2312.
原有的英文軟體可以很好的運行,C的庫函數也不用做修改, 比如 strlen("ABC") 在GB2312表示的內碼中, 由於GB2312對英文字元的編碼是和ASCII完全一樣的,所以返回
3.對於 strlen("A漢字"), 由於strlen()是以內碼為0作為邊界的,而所有中文字元的GB2312內碼高位都為1,不會出現0,並且每個漢字占用2個位元組,所以 strlen 返回5. 對於程式來說只要檢查一個位元組的最高位,就可以很容易的判斷這個字元是中文還是英文字元,非常方便.
"一個字母一個位元組,一個漢字2個位元組" 的觀念深入人心.
有了GB2312之後,漢字顯示/存儲/交換就基本上沒什麼問題了.
幾乎所有的非英語國家都制定了和GB2312類似相容ASCII的內碼字元對應表.
(BIG5 由於有幾個字元的內碼和ASCII相同但表示不同的字元,不符合2.(1)條件.所以被認為是有"瑕疵"的.)
3. 很明顯,GB2312的碼位是不夠的, 一個例子就是有很多人的人名電腦里打不出來.(只有6000多個碼位,而<<康熙字典>>就收錄了4萬多個漢字).所以後來有出現了諸如GBK, GB18030以及同期流行於臺灣香港的BIG5編碼. 雖然編碼有些不同, 但是設計思想是一致的: 相容ASCII,並確保不會有某個位元組值為0的內碼出現.有一個共同的特點是: 它們都是局部的標準,只流行於某個地區/國家內.
4.由於內碼表都是各個國家獨自製定的,同一個內碼,在不同的國家表示的可能是不同的字元.(除了ASCII字元, ASCII字元在所有國家指定的內碼表中都有同樣的值.)不利於國家間的信息交換. 於是 UNICODE 應運而生.
UNICODE 採用一種很簡單的辦法來解決這個問題. 就是採用2個 - UCS-2 (或者4個位元組 - UCS-4)位元組標識一個字元. 2個位元組總共可以表示65535個字元,足夠表示世界上的所有語言的所有字元.(漢字不就有4萬多個嗎,65535怎麼夠. 我估計只是常用的漢字幾千個被編在UCS-2中吧. 目前被正式編碼到UNICODE碼位的只有不超過65534個, 所以就目前的情況來說,用2個位元組是可以的.) 註意 UCS-4, UCS-2 和 ASCII是向下相容的,只要前面補0就可以了.這點很重要,可以一直擴展下去包含全宇宙的字元.
現在地球上每個字元在所有採用UNICODE字元編碼的電腦內都有一個唯一的內碼了.
要註意, 除了ASCII字元外,其他國家文字的字元的內碼是重新分配過的,不一定和各國原有的編碼相同.比如大部分漢字的GB2312內碼和UNICODE 內碼都是不同的.
5. 很顯然,對於英語國家來說,UNICODE內碼非常浪費空間,對於UCS-2 浪費了50%的存儲空間,對於 UCS-4 則浪費了70%的存儲空間. 而且還有一個更大的問題, UNICODE的內碼中含有很多 '\0', 原有的C標準庫函數沒辦法處理這些字元串.於是有人發明瞭一種針對UNICODE的變換規則,把UNICODE字元串中的0去除. 註意這個變換規則不是通過查表實現的,而只要用一些位移操作就可以實現. 這就是UTF8.
總結:
UTF8 只是 UNICODE內碼在存儲/傳輸時的狀態. 而從GB2312編碼轉換到UNICODE編碼需要查表. UTF8 和 UNICODE 的關係 與 GB2312 和 UNICODE的關係有本質的不同. UTF8 和 UNICODE 是一個人的兩個面孔, GB2312 和 UNICODE 是兩個人.
所以,要實現UTF8編碼到GB2312編碼的轉換必須先把 UTF8編碼還原為UNICODE編碼,再通過查表的方式,把UNICODE編碼轉化為GB2312編碼.
以上,雖然說得不是很嚴謹(比如GB2312其實是區位碼,真正的內碼還要給每個位元組加上A0, 這些我都沒提,免得分散註意力),但是文字編碼的原理大致就是那麼回事,理解就好了. 要想詳細瞭解細節Google一下能找到很多資料.
二、字元編碼的編程相關問題
1. Windows從NT開始,內核使用UNICODE內碼. 為了向前相容,前端使用的還是GB2312內碼(中文環境).
所以用 Visual Studio 編寫代碼時, 如果在CPP文件中寫這樣一句 const char* pszText = "中文", 編譯器讓 pszText 指向"中文"的GB2312內碼值的記憶體空間. 當調用 printf(pszText)時, WINAPI 把這個GB2312字元串轉化為UNICODE字元串再輸出.(WIndows自然知道你的編碼是GB2312,因為你在Windows系統中設置的語言區域是中國, CodePage 936. 如果改成其它語言,就會顯示為亂碼.)
微軟非常鼓勵Windows程式員用Unicode編寫程式,很明顯,由於Windows內核就是原生的Unicode環境,調用API時,省卻了編碼轉換的操作,效率更高. 而且一個額外的好處就是不會有亂碼. 註意,MS的C/C++編譯器把sizeof(wchar_t)設置為2個位元組. 由於目前所有的UNICODE字元只有65534個碼位(BMP),所以用2個位元組是沒問題的.
2. Linux系統(比如Ubuntu)現在一般都用UTF8編碼了.
我們在Linux下創建CPP文件並添加同樣的: const char* pszText = "中文" 編譯器會讓 pszText 指向"中文"UTF8的內碼值的記憶體空間.Linux的終端可以理解為一個只接收UTF8字元串的顯示器. 任何被寫到終端的字元流都被認為是是一個UTF8字元流.所以,編程的時候,從外部(文件或者控制台)讀入UTF8字元流,轉換為wchar_t,然後程式在內部使用寬字元處理,最後再把要輸出的寬字元流轉換為UTF8字元流並輸出到控制台/文件中. 用戶程式可以通過環境變數LANG的值得知當前的系統環境所使用的字元編碼.由此可見,C庫函數的 mbstowcs()/wcstombs()主要是為應付這種情況設計的. 如果要處理XML, HTML 等等有明確指明字元編碼的字元流,用專門的字元轉換庫更為方便.
為什麼很多Windows下的C源文件的註釋在Linux編輯器下會顯示為亂碼就很好理解了.
3. 字元編碼轉換相關的函數和庫
Windows 的字元轉換函數: MultiByteToWideChar() / WideCharToMultiByte()
Linux 的字元轉換庫: GLIBC iconv函數組.C標準庫使用的 mbstowcs()和wcstombs()和 locale 相關,用起來很不方便,而且功能有限.
(註意不要假設 wchar_t 的大小, 它可能是4位元組也可能是2位元組,取決於編譯器. 比如 MS VC9.0 (2008) 里, sizeof(wchar_t) = 2, 而在GCC中, sizeof(wchar_t) = 4.)
4. 給定一個ANSI相容的字元串(包括GB2312,GBK,UTF8等),無法確定它的編碼類型,只能猜測.所以不要指望會有一個萬能的轉換函數.
5. BOM (Byte Order Mark)UNICODE: FF FE / FE FF 和 UTF8: EF BB BF 是不完全靠譜的,僅供參考.
最後說明一點,對於不是專門處理字元編碼的程式來說,所有字元編碼相關的問題只是顯示的問題,並不會影響到程式的內在邏輯.
開始用 Unicode 來編寫我們的代碼吧.