天天看點

《轉》c++ 字元串系列:字元編碼進階(上)

所有的 string 類都是以C-style字元串為基礎的。C-style 字元串是字元數組。是以我們先介紹字元類型。這裡有3種編碼模式對應3種字元類型。

第一種編碼類型是單子節字元集(single-byte character set or SBCS)。在這種編碼模式下,所有的字元都隻用一個位元組表示。ASCII是SBCS。一個位元組表示的0用來标志SBCS字元串的結束。

第二種編碼模式是多位元組字元集(multi-byte character set or MBCS)。一個MBCS編碼包含一些一個位元組長的字元,而另一些字元大于一個位元組的長度。用在Windows裡的MBCS包含兩種字元類型,單位元組字元(single-byte characters)和雙位元組字元(double-byte characters)。由于Windows裡使用的多位元組字元絕大部分是兩個位元組長,是以MBCS常被用DBCS代替。

  在DBCS編碼模式中,一些特定的值被保留用來表明他們是雙位元組字元的一部分。例如,在Shift-JIS編碼中(一個常用的日文編碼模式),0x81-0x9f之間和 0xe0-oxfc之間的值表示"這是一個雙位元組字元,下一個子節是這個字元的一部分。"這樣的值被稱作"leading bytes",他們都大于0x7f。跟随在一個leading byte子節後面的位元組被稱作"trail byte"。在DBCS中,trail byte可以是任意非0值。像SBCS一樣,DBCS字元串的結束标志也是一個單位元組表示的0。

第三種編碼模式是Unicode。Unicode是一種所有的字元都使用兩個位元組編碼的編碼模式。Unicode字元有時也被稱作寬字元,因為它比單子節字元寬(使用了更多的存儲空間)。注意,Unicode不能被看作MBCS。MBCS的獨特之處在于它的字元使用不同長度的位元組編碼。Unicode字元串使用兩個位元組表示的0作為它的結束标志。

  單位元組字元包含拉丁文字母表,accented characters及ASCII标準和DOS作業系統定義的圖形字元。雙位元組字元被用來表示東亞及中東的語言。Unicode被用在COM及Windows NT作業系統内部。

  你一定已經很熟悉單位元組字元。當你使用char時,你處理的是單位元組字元。雙位元組字元也用char類型來進行操作(這是我們将會看到的關于雙子節字元的很多奇怪的地方之一)。Unicode字元用wchar_t來表示。Unicode字元和字元串常量用字首L來表示。例如:

wchar_t wch = L''1''; // 2 bytes, 0x0031

wchar_t* wsz = L"Hello"; // 12 bytes, 6 wide characters

二.字元在記憶體中是怎樣存儲的

單位元組字元串:每個字元占一個位元組按順序依次存儲,最後以單位元組表示的0結束。例如。"Bob"的存貯形式如下:

42

6F

62

00

B

o

b

BOS

     Unicode的存儲形式,L"Bob"

42 00

6F 00

62 00

00 00

使用兩個位元組表示的0來做結束标志。

  一眼看上去,DBCS 字元串很像 SBCS 字元串,但是它使得使用字元串操作函數和永字元指針周遊一個字元串時會産生預料之外的結果。

字元串"日本語" ("nihongo")在記憶體中的存儲形式如下(LB和TB分别用來表示 leading byte 和 trail byte):

93 FA

96 7B

8C EA

LB TB

EOS

三.字元串處理函數

我們都已經見過C語言中的字元串函數,strcpy(), sprintf(), atoll()等。這些字元串隻應該用來處理單位元組字元字元串。标準庫也提供了僅适用于Unicode類型字元串的函數,比如wcscpy(), swprintf(), wtol()等。

  微軟還在它的CRT(C runtime library)中增加了操作DBCS字元串的版本。str***()函數都有對應名字的DBCS版本_mbs***()。如果你料到可能會遇到DBCS字元串(如果你的軟體會被安裝在使用DBCS編碼的國家,如中國,日本等,你就可能會),你應該使用_mbs***()函數,因為他們也可以處理SBCS字元串。(一個DBCS字元串也可能含有單位元組字元,這就是為什麼_mbs***()函數也能處理SBCS字元串的原因)

  讓我們來看一個典型的字元串來闡明為什麼需要不同版本的字元串處理函數。我們還是使用前面的Unicode字元串 L"Bob":

因為x86CPU是little-endian,值0x0042在記憶體中的存儲形式是42 00。你能看出如果這個字元串被傳給strlen()函數會出現什麼問題嗎?它将先看到第一個位元組42,然後是00,而00是字元串結束的标志,于是strlen()将會傳回1。如果把"Bob"傳給wcslen(),将會得出更壞的結果。wcslen()将會先看到0x6f42,然後是0x0062,然後一直讀到你的緩沖區的末尾,直到發現00 00結束标志或者引起了GPF。

  str***()函數根本不考慮DBCS字元,而_mbs***()考慮。如果,你調用strrchr("C:\ ", ''\''),傳回結果可能是錯誤的,然而_mbsrchr()将會認出最後的雙位元組字元,傳回一個指向真的''\''的指針。

  關于字元串函數的最後一點:str***()和_mbs***()函數認為字元串的長度都是以char來計算的。是以,如果一個字元串包含3個雙位元組字元,_mbslen()将會傳回6。Unicode函數傳回的長度是按wchar_t來計算的。例如,wcslen(L"Bob")傳回3。

四.Win32 API中的MBCS和Unicode

盡管你也許從來沒有注意過,Win32中的每個與字元串相關的API和message都有兩個版本。一個版本接受MBCS字元串,另一個接受Unicode字元串。例如,根本沒有SetWindowText()這個API,相反,有SetWindowTextA()和SetWindowTextW()。字尾A表明這是MBCS函數,字尾W表示這是Unicode版本的函數。

  當你 build 一個 Windows 程式,你可以選擇是用 MBCS 或者 Unicode APIs。如果,你曾經用過VC向導并且沒有改過預處理的設定,那表明你用的是MBCS版本。那麼,既然沒有 SetWindowText() API,我們為什麼可以使用它呢?

winuser.h頭檔案包含了一些宏,例如:

BOOL WINAPI SetWindowTextA ( HWND hWnd, LPCSTR lpString );

BOOL WINAPI SetWindowTextW ( HWND hWnd, LPCWSTR lpString );

#ifdef UNICODE

#define SetWindowText  SetWindowTextW

#else

#define SetWindowText  SetWindowTextA

#endif 

當使用MBCS APIs來build程式時,UNICODE沒有被定義,是以預處理器看到:

#define SetWindowText SetWindowTextA

  這個宏定義把所有對SetWindowText的調用都轉換成真正的API函數SetWindowTextA。(當然,你可以直接調用SetWindowTextA() 或者 SetWindowTextW(),雖然你不必那麼做。)

  是以,如果你想把預設使用的API函數變成Unicode版的,你可以在預處理器設定中,把_MBCS從預定義的宏清單中删除,然後添加UNICODE和_UNICODE。(你需要兩個都定義,因為不同的頭檔案可能使用不同的宏。) 然而,如果你用char來定義你的字元串,你将會陷入一個尴尬的境地。考慮下面的代碼:

HWND hwnd = GetSomeWindowHandle();

char szNewText[] = "we love Bob!";

SetWindowText ( hwnd, szNewText );

在預處理器把SetWindowText用SetWindowTextW來替換後,代碼變成:

SetWindowTextW ( hwnd, szNewText );

  看到問題了嗎?我們把單位元組字元串傳給了一個以Unicode字元串做參數的函數。解決這個問題的第一個方案是使用 #ifdef 來包含字元串變量的定義:

wchar_t szNewText[] = L"we love Bob!";

#endif

你可能已經感受到了這樣做将會使你多麼的頭疼。完美的解決方案是使用TCHAR。

繼續閱讀