1. 程式人生 > >UNICODE與多位元組字符集的區別及轉換

UNICODE與多位元組字符集的區別及轉換

一、一點歷史

在計算機中字元通常並不是儲存為影象,每個字元都是使用一個編碼來表示的,而每個字元究竟使用哪個編碼代表,要取決於使用哪個字符集(charset)。 在最初的時候,Internet上只有一種字符集——ANSI的ASCII字符集,它使用7 bits來表示一個字元,總共表示128個字元,其中包括了英文字母、數字、標點符號等常用字元。之後,又進行擴充套件,使用8 bits表示一個字元,可以表示256個字元,主要在原來的7 bits字符集的基礎上加入了一些特殊符號例如製表符。 後來,由於各國語言的加入,ASCII已經不能滿足資訊交流的需要,因此,為了能夠表示其它國家的文字,各國在ASCII的基礎上制定了自己的字符集,這些從ANSI標準派生的字符集被習慣的統稱為ANSI字符集,它們正式的名稱應該是MBCS(Multi-Byte Chactacter System,即多位元組字元系統)。

  這些派生字符集的特點是以ASCII 127 bits為基礎,相容ASCII 127,他們使用大於128的編碼作為一個Leading Byte,緊跟在Leading Byte後的第二(甚至第三)個字元與Leading Byte一起作為實際的編碼。這樣的字符集有很多,我們常見的GB-2312就是其中之一。 例如在GB-2312字符集中,“連通”的編碼為C1 AC CD A8,其中C1和CD就是Leading Byte。前127個編碼為標準ASCII保留,例如“0”的編碼是30H(30H表示十六進位制的30)。軟體在讀取時,如果看到30H,知道它小於128就是標準ASCII,表示“0”,看到C1大於128就知道它後面有一個另外的編碼,因此C1 AC一同構成一個整個的編碼,在GB-2312字符集中表示“連”。 由於每種語言都制定了自己的字符集,導致最後存在的各種字符集實在太多,在國際交流中要經常轉換字符集非常不便。因此,提出了Unicode字符集,它固定使用16 bits(兩個位元組、一個字)來表示一個字元,共可以表示65536個字元。將世界上幾乎所有語言的常用字元收錄其中,方便了資訊交流。標準的Unicode稱為UTF-16。後來為了雙位元組的Unicode能夠在現存的處理單位元組的系統上正確傳輸,出現了UTF-8,使用類似MBCS的方式對Unicode進行編碼。注意UTF-8是編碼,它屬於Unicode字符集。Unicode字符集有多種編碼形式,而ASCII只有一種,大多數MBCS(包括GB-2312)也只有一種。Unicode的最初目標,是用1個16位的編碼來為超過65000字元提供對映。但這還不夠,它不能覆蓋全部歷史上的文字,也不能解決傳輸的問題 (implantation head-ache's),尤其在那些基於網路的應用中。已有的軟體必須做大量的工作來程式16位的資料。因此,Unicode用一些基本的保留字元制定了三套編碼方式。它們分別是UTF-8,UTF-16和UTF-32。正如名字所示,在UTF-8中,字元是以8位序列來編碼的,用一個或幾個位元組來表示一個字元。這種方式的最大好處,是UTF-8保留了ASCII字元的編碼做為它的一部分,例如,在UTF-8和ASCII中,“A”的編碼都是0x41.UTF-16和UTF-32分別是Unicode的16位和32位編碼方式。

  考慮到最初的目的,通常說的Unicode就是指UTF-16。 例如“連通”兩個字的Unicode標準編碼UTF-16 (big endian)為:DE 8F 1A 90而其UTF-8編碼為:E8 BF 9E E9 80 9A 最後,當一個軟體開啟一個文字時,它要做的第一件事是決定這個文字究竟是使用哪種字符集的哪種編碼儲存的。軟體有三種途徑來決定文字的字符集和編碼: 最標準的途徑是檢測文字最開頭的幾個位元組,如下表:開頭位元組 Charset/encodingEF BB BF UTF-8FE FF UTF-16/UCS-2, little endianFF FE UTF-16/UCS-2, big endianFF FE 00 00 UTF-32/UCS-4, little endian.00 00 FE FF UTF-32/UCS-4, big-endian.例如**標記後,連通”兩個字的UTF-16 (big endian)和UTF-8碼分別為:FF FE DE 8F 1A 90EF BB BF E8 BF 9E E9 80 9A 但是MBCS文字沒有這些位於開頭的字符集標記,更不幸的是,一些早期的和一些設計不良的軟體在儲存Unicode文字時不**這些位於開頭的字符集標記。因此,軟體不能依賴於這種途徑。這時,軟體可以採取一種比較安全的方式來決定字符集及其編碼,那就是彈出一個對話方塊來請示使用者,例如將那個“連通”檔案拖到MS Word中,Word就會彈出一個對話方塊。 如果軟體不想麻煩使用者,或者它不方便向用戶請示,那它只能採取自己“猜”的方法,軟體可以根據整個文字的特徵來猜測它可能屬於哪個charset,這就很可能不準了。使用記事本開啟那個“連通”檔案就屬於這種情況。

  我們可以證明這一點:在記事本中鍵入“連通”後,選擇“Save As”,會看到最後一個下拉框中顯示有“ANSI”,這時儲存。當再當開啟“連通”檔案出現亂碼後,再點選“File”->“Save As”,會看到最後一個下拉框中顯示有“UTF-8”,這說明記事本認為當前開啟的這個文字是一個UTF-8編碼的文字。而我們剛才儲存時是用ANSI字符集儲存的。這說明,記事本猜測了“連通”檔案的字符集,認為它更像一個UTF-8編碼文字。這是因為“連通”兩個字的GB-2312編碼看起來更像UTF-8編碼導致的,這是一個巧合,不是所有文字都這樣。可以使用記事本的開啟功能,在開啟“連通”檔案時在最後一個下拉框中選擇ANSI,就能正常顯示了。反過來,如果之前儲存時儲存為UTF-8編碼,則直接開啟也不會出現問題。 如果將“連通”檔案放入MS Word中,Word也會認為它是一個UTF-8編碼的檔案,但它不能確定,因此會彈出一個對話方塊詢問使用者,這時選擇“簡體中文(GB2312)”,就能正常打開了。

  記事本在這一點上做得比較簡化罷了,這與這個程式的定位是一致的。需要提醒大家的是,部分Windows 2000字型無法顯示所有的Unicode字元。如果發現檔案中缺少了某些字元,只需將其變更為其它字型即可。big endian和little endianbig endian和little endian是CPU處理多位元組數的不同方式。例如“漢”字的Unicode編碼是6C49。那麼寫到檔案裡時,究竟是將6C寫在前面,還是將49寫在前面?如果將6C寫在前面,就是big endian。還是將49寫在前面,就是little endian。“endian”這個詞出自《格列佛遊記》。小人國的內戰就源於吃雞蛋時是究竟從大頭(Big-Endian)敲開還是從小頭(Little-Endian)敲開,由此曾發生過六次叛亂,其中一個皇帝送了命,另一個丟了王位。我們一般將endian翻譯成“位元組序”,將big endian和little endian稱作“大尾”和“小尾”。Unicode big endian:在Big-endian處理器(如蘋果Macintosh電腦)上建立的Unicode檔案中的文字位元組(存放單位)排列順序,與在Intel處理器上建立的檔案的文字位元組排列順序相反。最重要的位元組擁有最低的地址,且會先儲存文字中較大的一端。為使這類電腦的使用者能夠存取你的檔案,可選擇Unicode big-endian格式。

  應該說Unicode字符集更加通用,但這裡讀取目錄因為用到Windows.h裡面的一些東西,所以需要多位元組字符集,畢竟Microsoft的東西相容性一直不太好,所以如果可以的話,還是使用Unicode字符集比較好!

二、UNICODE與多位元組字符集

VS2008預設的字符集是Unicode,而VC6.0預設是多位元組字符集,Unicode字符集你要加_T("")或L"",你也可以“

工程-屬性-修改字符集”。


 1. UNICODE:它是用兩個位元組表示一個字元的方法。比如字元'A'在ASCII下面是一個字元,可'A'在UNICODE下面是兩個字元,高字元用0填充,而且漢字'程'在ASCII下面是兩個位元組,而在UNICODE下仍舊是兩個位元組。UNICODE的用處就是定長表示世界文字,據統計,用兩個位元組可以編碼現存的所有文字而沒有二義。 

 
 2. MBCS,它是多位元組字符集,它是不定長表示世界文字的編碼。MBCS表示英文字母時就和ASCII一樣(這也是我們容易把MBCS和ASCII搞混的原因),但表示其他文字時就需要用多位元組。

WINDOWS下面的程式設計可以支援MBCS和UNICODE兩種編碼的字串,具體用那種就看你定義了MBCS巨集還是UNICODE巨集。MBCS巨集對應的字串指標是char*也就是LPSTR,UNICODE對應的指標是unsigned   short*也就是LPWSTR,為了寫程式方便微軟定義了型別LPTSTR,在MBCS下他就是char*,   在UNICODE下它unsigned  

char*,這樣你就可以重定義一個巨集進行不同字符集的轉換了。


3. LPTSTR、LPCSTR、LPCTSTR、LPSTR的意義

LPSTR:  32-bit指標 指向一個字串,每個字元佔1位元組
LPCSTR:  32-bit指標 指向一個常字串,每個字元佔1位元組
LPCTSTR: 32-bit指標 指向一個常字串,每字元可能佔1位元組或2位元組,取決於Unicode是否定義
LPTSTR:  32-bit指標 每字元可能佔1位元組或2位元組,取決於Unicode是否定義

Windows使用兩種字符集ANSI和UNICODE,前者就是通常使用的單位元組方式,但這種方式處理象中文這樣的雙位元組字元不方便,容易出現半個漢字的情況。而後者是雙位元組方式,方便處理雙位元組字元。WindowsNT的所有與字元有關的函式都提供兩種方式的版本,而Windows9x只支援ANSI方式。_T一般同字常數相關,如_T("Hello"。如果你編譯一個程式為ANSI方式,_T實際不起任何作用。而如果編譯一個程式為UNICODE方式,則編譯器會把"Hello"字串以UNICODE方式儲存。_T和_L的區別在於,_L不管你是以什麼方式編譯,一律以UNICODE方式儲存.

 4. 例1:

Windows核心程式設計的第一章。

L是表示字串資源為Unicode的。

比如wchar_t Str[] = L"Hello World!";    這個就是雙子節儲存字元了。

_T是一個適配的巨集~

當#ifdef _UNICODE的時候_T就是L

沒有#ifdef _UNICODE的時候_T就是ANSI的。

比如

LPTSTR lpStr = new TCHAR[32];
TCHAR* szBuf = _T("Hello");
以上兩句使得無論是在UNICODE編譯條件下都是正確編譯的。

而且MS推薦你使用相匹配的字串函式。
比如處理LPTSTR或者LPCTSTR 的時候,不要用strlen ,而是要用_tcslen

否則在UNICODE的編譯條件下,strlen不能處理 wchar_t*的字串。

T是非常有意思的一個符號(TCHAR、LPCTSTR、LPTSTR、_T()、_TEXT()...),它表示使用一種中間型別,既不明確表示使用 MBCS,也不明確表示使用 UNICODE。那到底使用哪種字符集?編譯的時候才決定


在大多數情況下,CString 轉換成 LPTSTR是非常容易的,如果函式要求傳入LPTSTR型的引數,直接傳一個

CString也行,但是在visual studio 2008中,卻偶爾會出現不能轉換的情況,這個為什麼呢?

有人以為這是ASCII(多位元組)與Unicode(寬位元組)之間的問題,其實不是,要知LPTSTR這個巨集是隨編譯器引數不同而不同的,如果在編譯器——常規裡面設定程式按ASCII編譯,那LPTSTR就表示char*,如果選擇Unicode

編譯那就是wchar_t*。CString也是如此,隨編譯器選項的不同,可以是ASCII字串也可以是Unicode字串。

那麼CString與LPTSTR,要麼全是多位元組,要麼全是寬位元組,不可能存在兩者之間不能轉換的問題。

例2:

1. 如何將 CString 型轉換為 LPBYTE 
CString   str;   
LPBYTE   by   =   (LPBYTE)(LPCSTR)str;

2. LPBYTE 如何轉為CString 型

CString   str;

str.Format("%s", by);

在vc++中有著各種字串的表示法,如您所說。             首先char*   是指向ANSI字元陣列的指標,其中每個字元佔據8位(有效資料是除掉最高位的其他7位),這裡保持了與傳統的C,C++的相容。        LP的含義是長指標(long   pointer)。 LPSTR是一個指向以‘’結尾的ANSI字元陣列的指標,與char*可以互換使用,在win32中較多地使用LPSTR。而LPCSTR中增加的‘C’的含義是“CONSTANT”(常量),表明這種資料型別的例項不能被使用它的API函式改變,除此之外,它與LPSTR是等同的。         為了滿足程式程式碼國際化的需要,業界推出了Unicode標準,它提供了一種簡單和一致的表達字串的方法,所有字元中的位元組都是16位的值,其數量也可以滿足差不多世界上所有書面語言字元的編碼需求,開發程式時使用Unicode(型別為wchar_t)是一種被鼓勵的做法。         LPWSTR與LPCWSTR由此產生,它們的含義類似於LPSTR與LPCSTR,只是字元資料是16位的wchar_t而不是char。         然後為了實現兩種編碼的通用,提出了TCHAR的定義:    如果定義_UNICODE,宣告如下:     typedef   wchar_t   TCHAR;     如果沒有定義_UNICODE,則宣告如下:     typedef   char   TCHAR;      LPTSTR和LPCTSTR中的含義就是每個字元是這樣的TCHAR。        CString類中的字元就是被宣告為TCHAR型別的,它提供了一個封裝好的類供使用者方便地使用。

注意:

這兩個函式是由Windows提供的轉換函式,不具有通用性

C語言提供的轉換函式為mbstowcs()/wcstombs()

一、函式簡單介紹

涉及到的標頭檔案:

函式所在標頭檔案:windows.h

#include <windows.h>

wchar_t型別所需標頭檔案:wchar.h

#include <wchar.h>

( 1 ) MultiByteToWideChar()

函式功能:該函式對映一個字串到一個寬字元(unicode)的字串。由該函式對映的字串沒必要是多位元組字元組。 

函式原型: 

int MultiByteToWideChar(

  UINT CodePage,   DWORD dwFlags,   LPCSTR lpMultiByteStr,   int cchMultiByte,   LPWSTR lpWideCharStr,   int cchWideChar   );

引數:

1> CodePage:指定執行轉換的多位元組字元所使用的字符集

這個引數可以為系統已安裝或有效的任何字符集所給定的值。你也可以指定其為下面的任意一值:

Value Description
CP_ACP ANSI code page
CP_MACCP Not supported
CP_OEMCP OEM code page
CP_SYMBOL Not supported
CP_THREAD_ACP Not supported
CP_UTF7 UTF-7 code page
CP_UTF8 UTF-8 code page
2> dwFlags:一組位標記,用以指出是否未轉換成預作或寬字元(若組合形式存在),是否使用象形文字替代控制字元,以及如何處理無效字元。你可以指定下面是標記常量的組合,含義如下:   MB_PRECOMPOSED:通常使用預作字元——就是說,由一個基本字元和一個非空字元組成的字元只有一個單一的字元值。這是預設的轉換選擇。不能與MB_COMPOSITE值一起使用。   MB_COMPOSITE:通常使用組合字元——就是說,由一個基本字元和一個非空字元組成的字元分別有不同的字元值。不能與MB_PRECOMPOSED值一起使用。   MB_ERR_INVALID_CHARS:如果函式遇到無效的輸入字元,它將執行失敗,且GetLastErro返回ERROR_NO_UNICODE_TRANSLATION值。   MB_USEGLYPHCHARS:使用象形文字替代控制字元。  組合字元由一個基礎字元和一個非空字元構成,每一個都有不同的字元值。每個預作字元都有單一的字元值給基礎/非空字元的組成。在字元è中,e就是基礎字元,而重音符標記就是非空字元。  標記MB_PRECOMPOSED和MB_COMPOSITE是互斥的,而標記MB_USEGLYPHCHARS和MB_ERR_INVALID_CHARS則不管其它標記如何都可以設定。 
一般不使用這些標誌,故取值為0時。
3> lpMultiByteStr:指向待轉換的字串的緩衝區。  4> cchMultiByte:指定由引數lpMultiByteStr指向的字串中位元組的個數。可以設定為-1,會自動判斷lpMultiByteStr指定的字串的長度 (如果字串不是以空字元中止,設定為-1可能失敗,可能成功),此引數設定為0函式將失敗。  5> lpWideCharStr:指向接收被轉換字串的緩衝區。  6> cchWideChar:指定由引數lpWideCharStr指向的緩衝區的寬位元組數。若此值為0,函式不會執行轉換,而是返回目標快取lpWideChatStr所需的寬字元數。 返回值:

如果函式執行成功,並且cchWideChar不為0,返回值是由lpWideCharStr指向的緩衝區中寫入的寬字元數;

如果函式執行成功,並且cchMultiByte為0,返回值是待轉換字串的緩衝區所需求的寬字元數大小。(此種情況用來獲取轉換所需的wchar_t的個數)

如果函式執行失敗,返回值為零。

若想獲得更多錯誤資訊,請呼叫GetLastError()函式。它可以返回下面所列錯誤程式碼:

  ERROR_INSUFFICIENT_BUFFER;     ERROR_INVALID_FLAGS;   ERROR_INVALID_PARAMETER;         ERROR_NO_UNICODE_TRANSLATION。 ( 2 ) WideCharToMultiByte()

函式功能:該函式對映一個unicode字串到一個多位元組字串。 

函式原型: 

int WideCharToMultiByte(

  UINT CodePage,   DWORD dwFlags,   LPCWSTR lpWideCharStr,   int cchWideChar,   LPSTR lpMultiByteStr,   int cchMultiByte,   LPCSTR lpDefaultChar,   LPBOOL pfUsedDefaultChar   );

引數:

與MultiByteToWideChar()函式中的引數類似,但是多了兩個引數:

lpDefaultCharpfUsedDefaultChar:只有當WideCharToMultiByte函式遇到一個寬位元組字元,而該字元在uCodePage引數標識的內碼表中並沒有它的表示法時,WideCharToMultiByte函式才使用這兩個引數。(通常都取值為NULL)

1> 如果寬位元組字元不能被轉換,該函式便使用lpDefaultChar引數指向的字元。如果該引數是NULL(這是大多數情況下的引數值),那麼該函式使用系統的預設字元。該預設字元通常是個問號。這對於檔名來說是危險的,因為問號是個萬用字元。

2> pfUsedDefaultChar引數指向一個布林變數,如果Unicode字串中至少有一個字元不能轉換成等價多位元組字元,那麼函式就將該變數置為TRUE。如果所有字元均被成功地轉換,那麼該函式就將該變數置為FALSE。當函式返回以便檢查寬位元組字串是否被成功地轉換後,可以測試該變數。

返回值

如果函式執行成功,並且cchMultiByte不為零,返回值是由 lpMultiByteStr指向的緩衝區中寫入的位元組數;

如果函式執行成功,並且cchMultiByte為零,返回值是接收到待轉換字串的緩衝區所必需的位元組數。(此種情況用來獲取轉換所需Char的個數)

如果函式執行失敗,返回值為零。

若想獲得更多錯誤資訊,請呼叫GetLastError函式。它可以返回下面所列錯誤程式碼:

  ERROR_INSUFFICIENT_BJFFER;ERROR_INVALID_FLAGS;   ERROR_INVALID_PARAMETER;ERROR_NO_UNICODE_TRANSLATION。

二、使用方法

( 1 ) 將多位元組字串轉為寬字串:

1) 呼叫MultiByteToWideChar()函式,設定cchWideChar引數為0(用以獲取轉換所需的接收緩衝區大小);

2) 獲取輸入快取的大小,作為cchMultiByte的值;(這樣做是為了節省空間,也可以給cchMultiByte取值-1(字串需要以空字元結尾,否則會出錯))

3) 分配足夠的記憶體塊,用於存放轉換後的Unicode字串;

該記憶體塊的大小由前面對cchWideChar()函式的返回值來決定;(也可以用別的方法,但該方法更節省記憶體)

4) 再次呼叫MultiByteToWideChar()函式,這次將快取的地址作為lpWideCharStr,引數來傳遞,並傳遞第一次呼叫MultiByteToWideChar()函式時返回值作為cchWideChar引數的值;

5) 使用轉換後的字串;

6) 釋放接收緩衝區佔用的記憶體塊;

示例程式碼:

  1. void main()  
  2. {  
  3.     char sBuf[25]={0};  
  4.     strcpy(sBuf, "我最棒");  
  5.     //獲取輸入快取大小
  6.     int sBufSize=strlen(sBuf);  
  7.     //獲取輸出快取大小
  8.     //VC++ 預設使用ANSI,故取第一個引數為CP_ACP
  9.     DWORD dBufSize=MultiByteToWideChar(CP_ACP, 0, sBuf, sBufSize, NULL, 0);  
  10.     printf("需要wchar_t%u個\n", dBufSize);  
  11.     wchar_t * dBuf=newwchar_t[dBufSize];  
  12.     wmemset(dBuf, 0, dBufSize);  
  13.     //進行轉換
  14.     int nRet=MultiByteToWideChar(CP_ACP, 0, sBuf, sBufSize, dBuf, dBufSize);  
  15.     if(nRet<=0)  
  16.     {  
  17.         cout<<"轉換失敗"<<endl;  
  18.         DWORD dwErr=GetLastError();  
  19.         switch(dwErr)  
  20.         {  
  21.         case ERROR_INSUFFICIENT_BUFFER:  
  22.             printf("ERROR_INSUFFICIENT_BUFFER\n");  
  23.             break;  
  24.         case ERROR_INVALID_FLAGS:  
  25.             printf("ERROR_INVALID_FLAGS\n");  
  26.             break;  
  27.         case ERROR_INVALID_PARAMETER:  
  28.             printf("ERROR_INVALID_PARAMETER\n");  
  29.             break;  
  30.         case ERROR_NO_UNICODE_TRANSLATION:  
  31.             printf("ERROR_NO_UNICODE_TRANSLATION\n");  
  32.             break;  
  33.         }  
  34.     }  
  35.     else
  36.     {  
  37.         cout<<"轉換成功"<<endl;  
  38.         cout<<dBuf;   
  39.     }  
  40.     delete(dBuf);  
  41. }  
注意:兩次呼叫MultiCharToWideChar()時,形參cchMultiByte的取值需要相同,否則可能會出現接收快取不足之類的錯誤,從而導致轉換失敗!

 ( 2 ) 從寬位元組轉為窄位元組字串

步驟與(1)類似,故不贅述

程式碼示例如下:

  1. //從寬字串轉換窄字串
  2. wchar_t sBuf[25]={0};  
  3. wcscpy(sBuf, L"我最棒");  
  4. //獲取轉換所需的目標快取大小
  5. DWORD dBufSize=WideCharToMultiByte(CP_OEMCP, 0, sBuf, -1, NULL,0,NULL, FALSE);  
  6. //分配目標快取
  7. char *dBuf = newchar[dBufSize];  
  8. memset(dBuf, 0, dBufSize);  
  9. //轉換
  10. int nRet=WideCharToMultiByte(CP_OEMCP, 0, sBuf, -1, dBuf, dBufSize, NULL, FALSE);  
  11. if(nRet<=0)  
  12. {  
  13.     printf("轉換失敗\n");  
  14. }  
  15. else
  16. {  
  17.     printf("轉換成功\nAfter Convert: %s\n", dBuf);  
  18. }  
  19. delete []dBuf;  

三、MultiByteToWideChar()函式亂碼的問題

有的朋友可能已經發現,在標準的WinCE4.2或WinCE5.0 SDK模擬器下,這個函式都無法正常工作,其轉換之後的字元全是亂碼!

及時更改MultiByteToWideChar()引數也依然如此。不過這個不是程式碼問題,其結症在於所定製的作業系統.如果我們定製的作業系統預設語言不是中文,也會出現這種情況。

由於標準的SDK預設語言為英文,所以肯定會出現這個問題。而這個問題的解決,不能在簡單地更改控制面板的"區域選項"的"預設語言",而是要在系統定製的時候,選擇預設語言為"中文"。系統定製時選擇預設語言的位置於:   Platform -> Setting... -> locale -> default language ,選擇"中文",然後編譯即可。