天天看點

字元編碼

這篇文章是我兩年多前寫給同僚看的,當時不少同僚對編碼了解甚少,直到現在發現還是很多人對編碼了解甚少,是以我就把這篇文章發出來讓大家參考一下,希望對一些人有幫助,不過這篇文章是當時花了3個小時左右寫的,錯誤在所難免。

字元編碼曆史

計算機,發明在20世紀中期西方國家。計算機内部使用二進制作為表示任何東西的基礎,為了能夠在計算機中使用整數、浮點數等都要對其進行編碼,隻是這個編碼是在硬體層的(CPU指令),而計算機要與人進行互動就要對人所能識别的文字進行編碼,ASCII就在那個時候誕生。

ASCII(American Standard Code for Information Interchange,美國資訊互換标準代碼)

美國标準編碼,用于編碼英文字母的編碼方式。它用了0-127的數字之間來表示a-z等可見字元和一些控制字元,而這個字元集的編碼就确定了ASCII字元集,而這個字元集要想要在計算機的二進制方式下使用就必須用計算機能夠了解的方式來處理,ASCII使用了一個計算機位元組來表示這種編碼方式,在C/C++語言中一個位元組通常我們使用char來表示。

GBK

GBK,大家都知道,漢字内碼擴充規範。GBK是怎麼來的呢?GBK由GB2312擴充來的,GB2312是最早的中文編碼方式。GB2312又是怎麼來的呢?

計算機發展到80年代在中國開始慢慢的興起。為了能夠讓中國人能夠更好的使用計算機,自然要引入中文編碼到計算機中。但是引入中文編碼遇到了一個問題,就是ASCII使用一個位元組(char)來編碼字元,但是中國的漢字是肯定不能夠在一個位元組中表示完全,怎麼辦?聰明的中國人發現一個位元組(char)可表示的最大區間是0-255,而ASCII隻使用了0-127,128-255并沒有使用,那麼我們就可以用多個位元組來表示中文編碼,比如:

一個位元組(char)的值是在0-127之間,我們就還是用這個位元組來表示ASCII裡面的字元,也就是說相容ASCII。

一個位元組(char)的值是在128-255之間(此處假設是130),則說明它不是在ASCII字元集裡面的,那它表示什麼字元呢?此種情況下,則要在讀取下一個位元組(char)的值(此處假設是10),那麼就将兩個位元組的值:130和10按照某種計算規則來計算等到一個值,此處計算得到3290,那麼這個值可以在一個字元編碼表中去查,它可能得到某個漢字,此處假設為‘字’字,然後就可以在螢幕上顯示出來了。

(此處的計算都是假設,我沒有詳細查,以後有時間再更正,了解原理即可。也可以檢視維基百科了解詳細。)

GBK中有哪些字元?

GBK是微軟利用GB2312未使用的編碼空間,收入GB13000.1的全部字元制定而來。GBK是用來編碼中文漢字的,并且相容ASCII字元集。GBK中是不是隻有簡體中文呢?

因為簡體漢字和繁體漢字有很大一部分是相同,把常見的幾千個繁體中文裡面的漢字編碼進來也不會多多少,是以,GBK裡面也是有繁體中文漢字的。

除此之外還有什麼,因為現在簡體和繁體的常見字都編碼了,那日文裡面的大多數文字也都在其中,隻需要将日文的平假名和片假名(也就幾十個字元)編碼進來。

是以,GBK裡面是有常見的簡體中文、繁體中文、日文等字元在一起的字元集。

Big5等編碼

中國人發明了多位元組編碼的方式,但是隻能編碼中國漢字(包括簡繁和日文)。其他國家和中國台灣想要編碼文字自然就會采用相似的方式來編碼,Big5等編碼方式就産生了,它們同樣按照某種計算方式計算出值,此處假設還是3290,但是他們的編碼表和GBK不一樣,是以他們代表的文字就不在是‘字’了。

Windows ANSI編碼

GBK、Big5等編碼就是ANSI編碼,也叫本地碼。ANSI編碼就是本地碼的統稱,就是在什麼國家或地區就是什麼編碼。比如在中國的大陸地區就是GBK,在中國台灣就是Big5。

亂碼

亂碼,很常見也很煩的一個Bug,編碼Bug。

假設我們在簡體中文的機器上,那麼本地碼是GBK,我們用它編碼了一些文字儲存在txt檔案裡面,然後把它拷貝到繁體中文的機器上,此處的本地碼是Big5,然後打開這個檔案,假設我們使用記事本打開,它打開檔案使用的預設編碼就是本地碼,即Big5,按照Big5的方式計算值,并依次查字元編碼表,然後顯示出來。亂碼來了,本來在GBK中編碼的有意義的字句,在Big5下計算查表出來得到的是一些文字字元,而這些文字字元連接配接在一起沒有任何意思,是以亂碼。

亂碼和字型

亂碼是編碼引起的,而在文字顯示中是要去字型中查詢到某個字元的形狀,然後顯示,有可能出現字型裡面沒有這個文字,那麼就會用一個預設的字元顯示,此時給人的感覺也像亂碼,通常表現出很多個字元都是一個樣子(比如一個白框),但是它不是亂碼,隻是字型中沒有文字的資訊,換個字型就能顯示。

UNICODE

随着網際網路的發展,各個國家基本上都有自己的本地編碼ANSI編碼。為此,系統要支援多過的本地編碼,怎麼辦?引入代碼頁code page,根據代碼頁号去查相應的字元集,GBK的代碼頁就是CP936(有細微差别,詳細可以檢視維基百科)。

但是代碼頁還是不能将不同字元集中的字元在同一系統中顯示,比如:漢字和阿拉伯文不能同時顯示。UNICODE誕生了。

UNICODE就是要将所有的字元全部編碼在一個字元集裡面,比如1-10000編碼簡體中文,10001-20000編碼繁體中文,依次類推,這樣就構成了UNICODE字元集。但是UNICODE字元集并沒說要怎麼編碼,隻是說某個數字代表某個字元,即之規定了數字到字元的的字典,但是沒有規定在計算機中怎麼編碼。

UCS(Universal Character Set)/UTF(Unicode transformation format)

為了在計算機中編碼字元,就出現了UCS/UTF編碼,常見的有UTF-8,UTF-16(UCS-2),UTF-32(UCS-4)編碼。

UTF-8是類似GBK編碼的一種編碼,就是用多個位元組編碼計算出值然後查表,它可以是一個位元組(也就是相容ASCII)表示一個字元,可以是兩個、三個、四個或者更多個位元組根據計算得到某個值,然後去查UNICODE表得到某個字元,這樣就将所有字元進行了編碼。

UTF-16則至少是需要兩個位元組來表示,也就是說,可以由兩個位元組計算得到某個值,也可以是四個位元組、六個位元組、八個位元組計算出值然後查表得到字元。

UTF-32則至少是需要四個位元組表示,以此類推。

C/C++中的編碼

char在C/C++中表示一個位元組,通常也用它來表示編碼字元,如果它編碼的字元是ASCII編碼,則是每個位元組都表示一個字元,也就是說每個char表示一個字元。如果編碼是ANSI,此處假設是GBK編碼,那麼可以是一個char表示的字元,也可以是兩個char表示一個字元。如果是UTF-8編碼,那麼可以是一個char、兩個char、三個char或者更多來表示一個字元。

wchar_t是C/C++中的寬字元,标準沒有規定它占幾個位元組,隻是規定用來編碼unicode字元集,一個wchar_t在windows(wchar_t是UTF-16編碼)下面占2個位元組,在linux(wchar_t是UTF-32編碼)下面占4個位元組,用wchar_t來編碼unicode的話,常見字元都可以用一個wchar_t來表示。但是unicode字元集一直在擴充引入更多的字元,是以很有可能一個wchar_t(windows)不能表示出80001号字元,那麼也就出現兩個wchar_t表示一個字元,這也就正好符合UTF-16編碼的規則。

C/C++中的亂碼解決方法

亂碼其實是無解的。大多數軟體的軟體的處理方式就是隻處理UNICODE、UTF-8和ANSI編碼,是以一段文字的ANSI編碼和打開機器的本地碼不一樣那麼就必然出現亂碼,當然若人為的告訴軟體說這段文字是某個code page編碼的,那麼還是可以正确顯示,但是這是依靠人為操作了。

軟體裡面處理這個問題處理的最好的有一類軟體,就是浏覽器。浏覽器檢測文本編碼的方式通常就是猜,猜它是哪種編碼,猜完是哪種編碼之後就用相應的code page去查字元,然後顯示。那麼這個猜是不是亂猜呢?不是,是通過逐個位元組掃描進行統計,看看這段文本最可能是哪種編碼。當然這樣做也會有錯誤,那麼也一樣會出現亂碼,但是已經出現亂碼的幾率很低了。(想詳細了解可以檢視firefox和chrome的源碼)

看不懂比亂碼好

假設一個程式是用的是GBK編碼的字元串,那麼在一個日文作業系統(Windows)上,軟體的字元那将是亂碼,這給人一個很不好的感受。即使此軟體沒有日文版,但是如果能夠将簡體中文正确的顯示出來那還是要好上許多的,說不定使用軟體的人還是個懂中文的人。

C++裡怎麼做?Windows從NT開始就支援寬字元版本的API,對于所有使用API的地方都是用寬字元版本就能夠正常的顯示出文字了,在C++中就是使用wchar_t。比如:

wchar_t *wstr = L"中文";

然後用對應的寬字元版本的API來顯示出來就可以了。那是不是程式内部全部都應該使用wchar_t來表示字元?我個人推薦隻在Windows下運作的程式這麼做,也就是跟字元顯示不相關的東西也使用wchar_t來表示。當然,也可以根據情況在隻在顯示字元的時候通過調用MultiByteToWideChar将其它編碼的字元轉換成寬字元來顯示,這樣顯示不相關的字元就可以使用多位元組字元集(ANSI、UTF-8)了。

對于跨平台的軟體(Windows、Linux),我個人推薦使用UTF-8編碼的字元來作為内部處理的字元,這樣在隻需要在字元顯示的地方轉換成相應的編碼就可以了。當然主要還是在Windows上面做處理,調用MultiByteToWideChar将UTF-8轉換成寬字元然後顯示。

UTF-8在C++ 98中的表示

在目前的C++标準中,我們通常不能直接在代碼裡面寫出UTF-8編碼的字元串常量。

char *str = "中文"; // 對于VS,隻有源檔案是不帶BOM的UTF-8編碼時才是UTF-8字元串,對于帶BOM的UTF-8編碼或者GBK編碼的檔案都是GBK的字元串;

                    // 對于GCC,源檔案編碼是什麼那麼這個字元串的編碼就是什麼。

wchar_t *wstr = L"中文"; // 這裡使用的是unicode編碼

但是,因為ANSI編碼和UTF-8編碼都是相容ASCII編碼的,是以我們可以在代碼裡面這樣寫:

char *str = "abc";  // 此處的編碼是ASCII、ANSI、UTF-8

也就是上面這段字元是可以當成ANSI編碼也可以當成是UTF-8編碼的,那麼我們就可以将它當成UTF-8編碼來使用。是以在代碼裡面最好不要出現字母以外的字元。(當然,不考慮多語言版的話除外)

那我們要與使用者互動的時候不能是英文字母啊。我們可以從資源檔案中讀取,即我們可以将要顯示的字元放置到ini、XML以及其它文本檔案中,這些檔案以UTF-8編碼。這樣我們程式就從資源檔案中讀取這些UTF-8編碼的字元就可以了。這也就可以很好的做多語言版本了,隻要将資源檔案中的字元改成其它語言的字元就可以了,當然編碼還是UTF-8。(Windows下視窗相關的資源.rc也使用UNICODE編碼就行)

這樣做值得麼?值得不值得就看我們的程式是不是需要做多語言版,或者将來要不要做多語言版,如果要,這就是值得的,不要當然就無所謂了。

UTF-8在C++ 11中的表示

C++ 98中不能寫出UTF-8、UTF-16、UTF-32的字元串常量,C++ 11加入了新字元類型char16_t和char32_t,其相應的常量表示如下:

繼續閱讀