1. 程式人生 > 實用技巧 >字符集、字元編碼、國際化、本地化簡要總結(UNICODE/UTF/ASCII/GB2312/GBK/GB18030)

字符集、字元編碼、國際化、本地化簡要總結(UNICODE/UTF/ASCII/GB2312/GBK/GB18030)

PS:要轉載請註明出處,本人版權所有。

PS: 這個只是基於《我自己》的理解,

如果和你的原則及想法相沖突,請諒解,勿噴。

前置說明

  本文作為本人csdn blog的主站的備份。(BlogID=101

環境說明

  普通的linux 和 普通的windows。
  VS2015 和 GCC 7.0

前言


  曾記得,我在(https://blog.csdn.net/u011728480/article/details/100277582 《數與計算機 (編碼、原碼、反碼、補碼、移碼、IEEE 754、定點數、浮點數)》)裡面說過,計算機裡面儲存了數值和符號。數值包含定點數和浮點數。符號包含文字及其他符號。

  那符號在計算機中是怎麼表示的呢?這裡首先就要引出一個概念叫做字符集,就是“人為”記錄歸納的某種文字或者符號在這個集合中的索引(例如:Ascii 中的'a' 的索引為97)。有了這個字元集合後,我們怎麼在計算機中表示這個字元集合呢?畢竟我們設計這個字元集合就是為了在計算機裡面顯示相關的符號的。這裡我們就要引出一個字元編碼的概念,就是在計算機中怎麼表示相關的符號(例如:Ascii 我們發現基本的字符集只規定了128個。於是我們用一個位元組來標識這個字符集中的字元即可。)。注意這裡提到的Ascii即可指字元編碼也可指字符集,至少我是這樣理解的。

  一直以來,我反正只是大概模糊的記得,ascii 代表字母及其他基本字元。 gb2312/gbk/gb18030/big5 ... 代表的就是多位元組的中文及其他字元。unicode 和 utf-8 可以代表世界上大部分國家及地區的字元符號,同時unicode是兩位元組的,utf-8是多位元組的(注意,關於unicode的說法其實有點小毛病,詳見下文)。我早就想詳細的瞭解和記錄一下這些說法的含義,但是一直沒有一個較好的機會。

  最近做的一個專案裡面,要在記憶體裡面查詢和對比中文(c++ 裡面存的中文,lua要去找和對比),這NM可把我苦慘了,於是我一狠心,就要把這個問題搞明白,不求全部搞清楚,但求我能夠怎麼解決這個問題,以及以後遇到這些問題怎麼處理。

  下面是一些常見及不常見短語說明:

  1. UNICODE(The Unicode Consortium(統一碼聯盟,是大公司組合的聯盟),一種字符集)
  2. UCS(Universal Multiple Octet Coded Character Set(通用多字元編碼集,是ISO標準委員會提出的),一種字符集)
  3. UTF/UTF-8/UTF-16(一種基於UNICODE的字元編碼格式,UTF是UCS Transfer Format的簡寫)
  4. GB2312/GBK/GB18030(全國資訊科技標準化技術委員會出版的3個版本的字符集及字元編碼格式)
  5. ASCII(American Standard Code for Information Interchange,常見的基本英文編碼)
  6. DBCS(Double-byte character set)
  7. ANSI(不知道是什麼的簡寫,但是隻會出現在windows平臺)
  8. 本地化/國際化(本地化和國際化是一個比較坑的概念,本地化可以簡單理解為把文字和符號顯示給對應區域的人。 國際化就是把文字和符號顯示給儘量多的人。我也不知道這樣翻譯對不對QAQ)




常見的字符集及對應的字元編碼規則說明


  字符集是存放的人為定義的一個字元索引集合。 字元編碼是考慮怎麼把這個字元集合在計算機中表示出來。



常見中文字符集及字元編碼(GB2312/GBK/GB18030)

  關於GB2312/GBK/GB18030的詳細說明大家可以去網上找找資料詳細瞭解,他們有許多的歷史因素在裡面。我這裡就只做簡單的說明。

  首先GB2312/GBK/GB18030是三個國標的簡稱。是全國資訊科技標準化技術委員會參考或者說是對接ISO提出的字符集/字元編碼方法,然後出版的適合中國特色的字符集/字元編碼標準。注意這裡的GB2312/GBK/GB18030既可以稱作字符集也可以稱作字元編碼,我們好像常用是把這個三個當做字元編碼,但是沒有強調字符集是什麼,所以我覺得這個是三個即是字符集又是字元編碼。下面對這三個字元編碼規則進行簡單的說明,這些規則裡面可能有些歷史原因小故事在裡面,感興趣的人去網上找找看,我這裡不做無用功了。

  GB2312是我國第一個字符集/字元編碼。其使用2個位元組代表一個漢字,而且為了相容Ascii,約定兩個編碼字元都必須大於0xA0(每個位元組都大於127,可以區分出Ascii與GB2312)。也就是說,GB2312的編碼範圍為:0xA1A1~0xF7FE。而且由於標註出版的比較早,裡面只包含了常見的漢字和非漢字內容。

  GBK是對GB2312的擴充套件。同樣也是使用2個位元組代表一個漢字。首先GBK原封不動的繼承了GB2312的編碼,同時編碼範圍由0xA1A1~0xF7FE 擴充套件到0x8140~0xFEFE。多餘GB2312的這些編碼,又添加了一些cjk的漢字和符號,同時也提供了自定義文字區域編碼的。

  GB18030是2005年出的最新的中文字符集/中文字元編碼。它是變長位元組編碼方式,和utf系列很像。下面進行簡略的說明:

  1. 1位元組,0x00~0x7F 相容Ascii
  2. 2位元組,0x8140~0xFEFE 相容GBK
  3. 4位元組,0x81308130~0xFE39FE39 存放其他文字和符號,例如我國的少數民族的文字、繁體漢字、日韓漢字等等。

  這裡多說一句,採用變長編碼的原因是節約字元儲存空間或者說是為了節約網路傳輸頻寬。



常見的Unicode字符集與UTF系列編碼

  上面我們介紹了中文的字符集及字元編碼,可以想象的是,非中文,非英文地區的人,也會做和我們同樣的事情,他們會定義適合他們自己的字符集,並定義適合他們自己的字元編碼。那就直接炸裂了,因為每個地區都有自己的字符集和字元編碼,非常不適合各個地方的人們文字交流。與此同時,網路使得各個地方的人們交流更加的頻繁,於是有些人就不爽了,想定義一個字符集來包含全世界的所有字元,這樣人們交流的時候就不需要對字元進行專門的轉碼。

  於是國際標準委員會和一個叫做統一碼聯盟的組織分別起草了一個字符集,分別是UCS 和 UNICODE。後面考慮到大家都是做的同樣的事情,於是兩個字元集合並了,叫做UCS/UNICODE。我們常見的是UCS-2/UNICODE。這個字符集裡面包含了全世界大部分的文字和符號。其表示範圍大概是0x000000 到 0x10FFFF。 UNICODE 字元索引一般表示為U+0x00AAAA

  在定義UCS/UNICODE這個超大字符集後,肯定想定義一個字元編碼才符合這些組織的身份。於是產生了UTF字元編碼系列的格式。我們常見的就是UTF-8/UTF-16 BE/UTF-16 LE/UTF-32 BE/UTF-32 LE格式。

  UTF-32簡要說明:直接用4個位元組表示UNICODE字串, 例如索引U+0xABCDEFAA 就表示為0xABCDEFAA(BE 大端) 或者 0xAAEFCDAB(LE 小端)。

  UTF-16簡要說明(windows常用編碼,與UTF-32一樣有類似的位元組序存在):

  1. U+0x0000 到 U+0xFFFF 用2個位元組表示。
  2. U+0x1 0000 到 U+0x10 FFFF 用4個位元組表示。

  下面對UTF-8進行簡要的說明:

  1. 1位元組,0000/0000-0000/007F(hex), 二進位制填充方式:0xxx xxxx(binary)
  2. 2位元組,0000/0080-0000/07FF(hex), 二進位制填充方式:110x xxxx/10xx xxxx(binary)
  3. 3位元組,0000/0800-0000/7FFF(hex), 二進位制填充方式:1110 xxxx/10xx xxxx/10xx xxxx(binary)
  4. 4位元組,0001/0000-0010/FFFF(hex), 二進位制填充方式:1111 0xxx/10xx xxxx/10xx xxxx/10xx xxxx(binary)

  對應的編碼範圍是:

  1. 0~127
  2. 128~2047
  3. 2048~65535
  4. 65536以上

  UTF-8的實現方式就是查出字元索引:U+0xABCD(U+43981) ,可以看到落在的編碼範圍是3位元組範圍,也就是2048~65535。於是我們看到的二進位制還有16個位置,恰好,我們的編碼的二進位制也是16個。從左到右依次放入對應位置的x即可。0xABCD二進位制為:1010 1011 1100 1101, 對應的UTF-8編碼為: 1110 (1010)/10(10) (1111)/10(00) (1101)







本地化和國際化


  上面我們介紹了兩個系列的字符集和對應常用的字元編碼。GBXXX系列是CJK區域的多位元組編碼,UNICDOE/UTF系列是全球大多數通用字符集及編碼。那麼為了我們釋出的計算機檔案能夠在全世界方便的使用,我們有兩種方案:

  1. 使用區域性多位元組編碼,例如我們釋出的檔案,攜帶多種區域性字元編碼檔案(GBXXX/阿拉伯的編碼等等),在不同地區的電腦上,根據系統的地區(win和linux都有,很重要,設定區域),使用不同的區域字元編碼檔案進行顯示。
  2. 直接使用UNICDOE/UTF系列,全球通用。

  看起來,直接使用UNICDOE/UTF系列就完事兒了,花裡胡哨,弄那麼多。其實不然,你看了UTF-8,對我們中文區來說不公平,因為大部分都是3位元組,而對於Ascii區域來說,他們的UTF-8,大部分都是1位元組。這NM就坑了撒,難道我大中華的硬碟或者頻寬就無限了?其次,可能有些我們可以在GB系列裡面定義的偏門字元內容,可能UNICODE裡面沒有,對於一個足夠大的市場來說,如果連他們的文字元號都表示不完,那還玩個D。於是也需要有GB系列這種區域性的來補充,換句話說,就是看實際應用。這就是軟體本地化和國際化的意義,裡面最要命的就是字元問題。

生活中常見的幾個有趣小實驗(猜到就讓你嘿嘿嘿)


  下面我們做一些比較有趣,而且常見的小實驗。



VS的Unicode字符集 和 多字符集選項(cl.exe)

  在我們程式設計的時候,特別是要涉及中文程式設計的時候,很多時候需要和這個選項打交道,也就是如圖。那麼這兩個選項有啥區別呢?請聽我慢慢道來。

  這個選項的主要作用是用來幫助 cl.exe 確認啟用什麼樣的Api,也就是我們常說的W結尾的還是A結尾的Api。下面我們用下面的小程式來實驗一波。

#include <cstdio>
#include <windows.h>
int main(int argc, char * argv[]) {

	const char *_str = "臥槽";
	printf("_str's mem = %x %x %x %x\n", 0xFF & _str[0], 0xFF & _str[1], 0xFF & _str[2], 0xFF & _str[3]);
	const wchar_t *_str_1 = L"臥槽";
	printf("_str's mem = %x %x\n", 0xFFFF & _str_1[0], 0xFFFF & _str_1[1]);
	
	//MessageBoxW(NULL, L"臥槽", L"U", MB_OK);
	//MessageBoxA(NULL, "臥槽", "M", MB_OK);

	system("pause");
	return 0;
}

  執行以上程式碼我們可以得到下圖的內容。

  然後我們根據以上的內容,通過二進位制編輯器,構造了兩個txt檔案。然後通過vs code 不同解碼下開啟。才能夠得到正確文字內容。

  下面我們來解釋Window Api中 A系列和W系列的區別。 例如在MessageBoxW(NULL, L"臥槽", L"U", MB_OK)和 MessageBoxA(NULL, "臥槽", "M", MB_OK)中,我們傳入的引數一個是char *的,一個是wchar_t *,通過我們列印,可以發現對應的記憶體資料是完全不一樣的,也就是說對應的文字編碼是完全不同的。A系列對應的GB18030編碼(多位元組,區域編碼,不同地區,可能就不是gb系列的編碼了),W系列對應的UTF-16 BE編碼(UNICODE)。那麼,它們代表啥意思呢?

  如果我們用A系列的Api,那麼就是用的多位元組編碼,也就是對應的本地區域編碼,在我們這個CJK區域,能夠正常顯示文字,但是如果不在我們CJK區域的話,極有可能就出現亂碼。也就是說,通過A系列弄出來的程式,很有可能就只能夠在我們CJK區域使用,其他區域可能需要用原始碼,在其他對應區域的VS編譯一下,才能夠正常使用程式。

  如果我們用W系列的Api,那麼用的就是UTF-16 BE編碼,由於UNICODE是針對全球大多數語言來做的一個字符集,那麼意味著,我們這個程式只需要編譯一次,把二進位制分發到全世界大多數區域也能夠正常使用的。

GCC的-finput-charset/-fexec-charset=gbk選項

  首先GCC的預設把原始檔用UTF-8解碼,如果遇到不支援的字元,需要使用-finput-charset來幫助才行。然後,我們分別帶和不帶-fexec-charset=gbk編譯如下程式,並執行。

#include <cstdio>

int main(int argc, char * argv[]) {

	const char *_str = "臥槽";
	printf("_str's mem = %x %x %x %x %x %x\n", 0xFF & _str[0], 0xFF & _str[1], 0xFF & _str[2], 0xFF & _str[3], 0xFF & _str[4], 0xFF & _str[5]);
	const wchar_t *_str_1 = L"臥槽";
	printf("_str's mem = %x %x\n", 0xFFFF & _str_1[0], 0xFFFF & _str_1[1]);
	return 0;
}

  得到如圖的結果。

  從圖片結果我們可以知道,GCC對待字串的方式和CL.exe不是很一致,但是通過傳入相關引數,即可得到同樣的結果。這裡強調一下,-fexec-charset 引數相當於cl.exe的解碼設定,類似上文vs選項,我們可以知道GCC的多位元組預設編碼是UTF-8。同時,GCC和CL.exe一樣,對於char_t型別,都是使用的UTF-16 BE格式。

  這裡,我們通過如圖的編碼輸出,手動來轉換一下UTF-8和UNICODE,驗證我們之前說的規則是否正確。

  1. “臥” 對應的是U+005367,十進位制為U+21351,二進位制U+0101 0011 0110 0111,根據區域值,是3位元組模式,對應填入得到UTF-8二進位制 1110 0101 1000 1101 1010 0111,十六進位制為0xE58DA7
  2. “槽” 對應的是U+0069FD,十進位制為U+27133,二進位制U+0110 1001 1111 1101‬,根據區域值,是3位元組模式,對應填入得到UTF-8二進位制 1110 0110 1010 0111 1011 1101,十六進位制為0xE6A7BD
  3. 參考模式:1110 xxxx/10xx xxxx/10xx xxxx(binary)

  這裡,我們發現,算出來的值,完全和我們的前面說的規則一樣。

VS Debug模式下的“燙燙燙燙燙燙燙燙燙燙燙燙燙燙燙” 看似搞笑行為

  首先,我們在vs的debug模式下,執行如下程式。

#include <cstdio>

int main(int argc, char * argv[]) {
    
	const char *_str = "臥槽";
	printf("_str's mem = %x %x %x %x\n", 0xFF & _str[0], 0xFF & _str[1], 0xFF & _str[2], 0xFF & _str[3]);
	const wchar_t *_str_1 = L"臥槽";
	printf("_str's mem = %x %x\n", 0xFFFF & _str_1[0], 0xFFFF & _str_1[1]);
	char _test[10];
	for (int i = 0; i < 10; i++)
		printf("%x ", 0xFF&_test[i]);

	printf("\n%s", _test);
	return 0;
}

  得到如圖的結果。

  其實是由於vs 在debug模式下,會把我們為初始化的記憶體初始化為0xCC。而0xCCCC恰好是“燙”的GB18030編碼,所以在我們CJK區域列印是“燙”,在其他區域可能是其他的字元。





後記


  其實到了這裡,我已經解決了我想要解決的問題。因為我只要知道目標程式的記憶體中中文的具體編碼(OD或者CE等等),然後我就可以進行我想要的文字查詢。

  其實本文也解決了gcc生成的程式和cl.exe生成的程式字串交換的問題。一個預設用的utf-8,一個是本地編碼,對我們來說,就是GB系列。




打賞、訂閱、收藏、丟香蕉、硬幣,請關注公眾號(攻城獅的搬磚之路)

PS: 請尊重原創,不喜勿噴。

PS: 要轉載請註明出處,本人版權所有。

PS: 有問題請留言,看到後我會第一時間回覆。