《C++字符串完全指南--第一部分:win32?字符編碼》????
原作者:Michael?Dun
????? 譯??? 者:Dingqiao?Wang??
引言
毫無疑問,你肯定見過像TCHAR,?std::string,?BSTR等等這類字符串類型.也包括一些以_tcs開頭的奇怪的宏。也許你正盯著屏幕"哇哇"的發愁,然而閱讀完本文情況將會改觀。這篇指南概述了引入各種類型字符串的目的,展示了它們的一些簡單用法,同時描述了在必要的時候在它們之間如何進行轉換。
在第一部分,將會涉及三種類型的字符編碼。理解編碼方案的工作原理對你至關重要。即使你現在已經知道字符串是一個字符數組,還是看看這部分內容。一旦你閱讀了這些內容,你就清楚了這么多字符串類之間的聯系。
在第二部分,將描述字符串類本身,包括什么時候使用哪種類以及如何在他們之間進行轉換。
基本字符-----ASCII,DBCS,Unicode
所有的字符串類最終都歸結為C風格的字符串,而C風格的字符串就是字符數組,因此我首先介紹下字符類型。有三種編碼方案和三種字符類型。第一種方案是單字節編碼(single-byte?character?set,?or?SBCS).這種方案里,所有字符都正好是一個字節長。ASCII碼就是單字節編碼的例子。單字節字符串以一個字節的0做結束標志。
第二種編碼方案是多字節編碼(multi-byte?character?set,?or?MBCS).在多字節編碼中包含一些單字節長的字符,也包含其它的多余一個字節長度的字符。在Windows中使用的多字節編碼方案中包含兩種類型,單字節和雙字節類型。由于在Windows中使用到的最長的多字節字符也就是2個字節長,因此常常用雙字節字符集(double-byte?character?set,?or?DBCS)來代替MBCS.
在雙字節編碼方式中,一些值被保留來指示他們是雙字節的一部分。舉個例子,在Shift-JIS編碼(一種常用的日文編碼方案)中,介于0x81-0x9F?and?0xE0-0xFC之間的值就用來說明這是雙字節字符,它的下一個字節是其一部分。這些值被稱作"頭部字節"(lead?bytes),他們總是比0X7F大。緊跟在頭部字節后的下一字節被稱作"后隨字節"(trail?bytes)。在雙字節編碼中,后隨字節可以為任意非零值。和單字節編碼一樣,雙字節編碼使用單字節的0值作為結束符。
第三種方案是Unicode。Unicode?是一種所有字符均采用二個字節的編碼標準。Unicode字符有時也被稱作寬字節(wide?characters),因為他們比單字節占用更多存貯。注意,Unicode并不是一種多字節編碼——多字節編碼的顯著特點是字符是不同長度的。一個Unicode字符串以兩個0值字節作為結束標志(0值的寬字符形式)。
單字節字符包括拉丁文字母,帶重音的字符(accented?characters),ASCII標準和DOS系統中定義的圖形符號。雙字節字符在東亞和中東地區的語言中使用。Unicode在COM和Windows?NT?內部使用。
你肯定已經很熟悉單字節字符了。當你在使用char類型時,處理的就是單字節字符。雙字節字符也用過char類型來操作(這也是我們使用雙字節時遇到的第一個怪現象)。wchar_t類型代表著Unicode字符。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 | EOS |
Unicode?版本的,L"Bob",是這樣存儲的:
42?00? | ?6F?00? | ?62?00? | ?00?00? |
B | o | b | EOS |
以0x0000(0的Unicode編碼形式)作為結束標記.
雙字節字符串初看起來像單字節字符串,但是當我們以后使用字符串操作函數和利用指針遍歷字符串時將看到他們的細微區別。字符串("nihongo")采用以下形式存貯(下面表中的LB代表?lead?bytes,TB代表trail?bytes):

記住,"ni"值并不是被解釋為0xFA93這一值。而是93和FA兩個值以那種字節序,在一起而被編碼為"ni".(因此在一個大端格式(Big-endian)的CPU上,這些字節仍然按上述順序)
字符串處理函數的使用
我們已經見過C風格字符串函數像strcpy(),?sprintf(),?atol()等等。這些函數只能用于處理單字節的字符串。標準庫中有他們的只能用于處理Unicode字符串的版本,諸如wcscpy(),?swprintf(),?_wtol().
微軟也在C運行庫(C runtime library)中增加了這些函數處理多字節字符串的版本。strxxx()這類函數對應的DBCS版本取名為_mbsxxx().如果你遇到了DBCS字符串(如果你的軟件是安裝在日文、中文或者其他使用DBCS的語言情況下你會遇到的),你應該總是使用_mbsxxx()函數,因為他們接受SBCS字符串(一個DBCS字符串可能僅僅包含單字節字符,這就是_mbsxxx()函數可以處理SBCS字符串的緣故)。
讓我們來看一個典型的字符串來解釋字符串處理函數不同版本的必要性。回到上文講到的Unicode字符串L"Bob":
?42?00? | ?6F?00? | ?62?00? | ?00?00? |
B | o | b | EOS |
因為x86系列CPU是小端格式(little-endian),值0x0042在內存中形式為42?00.你預見到了把這個字符串傳遞給函數strlen()的問題了嗎?函數將看到頭字節42,然后00,而00恰好是字符串結束標志,函數將返回1.相反,將"Bob"傳遞給函數wcslen(),將變得更糟。wcslen()會首先看到0x6F42,然后是0x0062,繼而一直讀下去直到碰到了00?00序列或者引起了GPF.
這里我們涉及到了strxxx()和wcsxxx()的對比。他們的區別又是什么呢?他們的區別至關重要,與在DBCS字符串中的合理的遍歷密切相關。下文將講述字符串的遍歷,然后再回到二者的對比上來。
字符串中合理的遍歷和索引
我們之中的大部分人都是伴著SBCS字符串而成長起來的,我們習慣了利用指針通過++和--操作符來遍歷一個字符串。我們也習慣于用數組來獲取字符串中的字符。這兩種方式在SBCS和Unicode字符串下用起來十分完美,因為字符都是相同長度的,編譯器會成功返回我們想要的字符。
但是,當你遇到了DBCS字符串時,為了代碼的正常運行,你必須改掉這種習慣。
這里有兩條利用指針遍歷DBCS字符串的原則。破壞了這些原則將導致你大部分與DBCS相關的漏洞(bugs)。
1.不要使用++操作符來向前遍歷,除非你一直檢查字符串的頭字節。
2.永遠不要用--操作符來向后遍歷。
我先解釋原則2,因為很容易找到一個破壞它的而不知不覺的例子。假設你有一個程序在自己的目錄里存貯配置文件,而你把安裝目錄寫入了注冊表里。在運行時,你讀取安裝目錄,附加上配置文件名,然后嘗試讀取它。再假設你的安裝目錄是C:\Program?Files\MyCoolApp,要建立的文件名是C:\Program?Files\MyCoolApp\config.bin,在你測試的時候它工作的很完美。
現在,假想以下是你用來建立文件名的代碼:
- bool?GetConfigFileName?(?char*?pszName,?size_t?nBuffSize?)??
- ??
- {??
- ??
- char?szConfigFilename[MAX_PATH];??
- ??
- ???
- ??
- ????//?Read?install?dir?from?registry...?we'll?assume?it?succeeds.??
- ??
- ???
- ??
- ????//?Add?on?a?backslash?if?it?wasn't?present?in?the?registry?value.??
- ??
- ????//?First,?get?a?pointer?to?the?terminating?zero.??
- ??
- char*?pLastChar?=?strchr?(?szConfigFilename,?'\0'?);??
- ??
- ???
- ??
- ????//?Now?move?it?back?one?character.??
- ??
- ????pLastChar--;????
- ??
- ???
- ??
- ????if?(?*pLastChar?!=?'\\'?)??
- ??
- ????????strcat?(?szConfigFilename,?"\\"?);??
- ??
- ???
- ??
- ????//?Add?on?the?name?of?the?config?file.??
- ??
- ????strcat?(?szConfigFilename,?"config.bin"?);??
- ??
- ???
- ??
- ????//?If?the?caller's?buffer?is?big?enough,?return?the?filename.??
- ??
- ????if?(?strlen?(?szConfigFilename?)?>=?nBuffSize?)??
- ??
- ????????return?false;??
- ??
- ????else??
- ??
- ????????{??
- ??
- ????????strcpy?(?pszName,?szConfigFilename?);??
- ??
- ????????return?true;??
- ??
- ????????}??
- ??
- }??
雖然這是一分很安全的代碼,但是遇到一些特殊的DBCS字符時,仍將會出錯。來分析下為什么會這樣,假設一個日本用戶將你的安裝目錄改為.以下是目錄名在內存中的存貯形式:
當GetConfigFileName()檢查反斜杠時,它會檢查安裝目錄的最后一個非0字節,來判斷是否等于"\\",如果沒有則添加上去。運行的結果是返回錯誤的文件名。哪兒出錯呢?看看以藍色高亮顯示的反斜杠。反斜杠字符的值是0x5C.的值是83 5C,而上述代碼誤將它的后隨字節當做了一個獨立字符。正確的向后遍歷方法是使用注意到DBCS字符特點的函數,使指針移動正確數目的字節。下面是正確的代碼,指針移動部分用紅色標記了。
- bool?FixedGetConfigFileName?(?char*?pszName,?size_t?nBuffSize?)??
- ??
- {??
- ??
- char?szConfigFilename[MAX_PATH];??
- ??
- ???
- ??
- ????//?Read?install?dir?from?registry...?we'll?assume?it?succeeds.??
- ??
- ??
- ????//?Add?on?a?backslash?if?it?wasn't?present?in?the?registry?value.??
- ??
- ????//?First,?get?a?pointer?to?the?terminating?zero.??
- ??
- ???char*?pLastChar?=?_mbschr?(?szConfigFilename,?'\0'?);??
- ??
- ??
- ????//?Now?move?it?back?one?double-byte?character.??
- ??
- ????pLastChar?=?CharPrev?(?szConfigFilename,?pLastChar?);??
- ??
- ????if?(?*pLastChar?!=?'\\'?)??
- ??
- ????????_mbscat?(?szConfigFilename,?"\\"?);??
- ??
- ???
- ????//?Add?on?the?name?of?the?config?file.??
- ??
- ????_mbscat?(?szConfigFilename,?"config.bin"?);??
- ??
- ?????//?If?the?caller's?buffer?is?big?enough,?return?the?filename.??
- ??
- ????if?(?_mbslen?(?szInstallDir?)?>=?nBuffSize?)??
- ??
- ????????return?false;??
- ??
- ????else??
- ??
- ????????{??
- ??
- ????????_mbscpy?(?pszName,?szConfigFilename?);??
- ??
- ????????return?true;??
- ??
- ????????}??
- ??
- }??
修改后的函數使用了CharPrev() API來使pLastChar向后移動一個字符,這樣就可能移動兩個字節如果字符串以雙字節字符結尾。在這個版本中,假設的情況會運行正常,因為頭部字節將永遠不等于0x5C。
你可以合理想象下破壞原則1的方式。舉個例子,你通過判斷字符':'出現的次數驗證用戶輸入的一個文件名是否合法。如果你使用++而不是CharNext()來遍歷,你可能會產生錯誤如果碰巧遇到后隨字節等于':'的字符。
和原則2相關的使用數組索引的原則:
2a.永遠不要使用減法來計算字符串的索引。
破壞這個原則的代碼和破壞原則2的代碼很相似。例如,pLastChar像下面這樣使用時:
- char*?pLastChar?=?&szConfigFilename?[strlen(szConfigFilename)?-?1];??
這同樣的破壞了原則,因為計算索引時使用減1這等于指針向后移動一個字節,這破壞了原則2.
再談strxxx()和_mbsxxx()的對比
現在應該明白_mbsxxx()這類函數的必要性了。Strxxx()不知道DBCS字符而_mbsxxx()函數了解.如果你調用將返回錯誤結果 ,但是_mbsxxx()將在末尾識別出雙字節字符,返回實際上指向反斜杠的指針。 關于字符串函數的最后一點,strxxx()和_mbsxxx()函數取或者返回長度均以char為單位。 因此對于一個包含3個雙字節字符的字符串,_mbslen()將返回6.Unicode函數以wchar_t為單位返回長度,例如wcslen(L"Bob")返回3.
Win32?API中的MBCS和Unicode
兩套API
即使你從沒有注意到,但是Win32中每一個處理字符串的API和消息都有兩個版本.
一個接受MBCS字符串,另一個接受Unicode字符串。舉個例子,并沒有SetWindowText這個API,相反,有SetWindowTextA()和SetWindowTextW().后綴A(對于ANSI)指示MBCS函數,后綴W(對于Wide)指示Unicode版本。
當你建立一個Windows應用程序,你可以選擇使用MBCS或者Unicode版本的API.如果你使用VC應用程序向導并且從未接觸過編譯器設置的話,你使用的一直是MBCS版本。那么為什么我們寫下"SetWindowText"而事實上又沒有這個名字對應的API呢?在winuser.h頭文件中包含了一些#define開頭的宏,如下:
- 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 API建立時,UNICODE就沒有定義,因此編譯器看到:
- #define?SetWindowText??SetWindowTextA??
并將所有調用SetWindowText()的地方用真正的API,SetWindowTextA來替換掉。(注意你可以直接調用函
數SetWindowTextA和SetWindowTextW,盡管你很少需要這樣做.)
因此,如果你想要把Unicode API設定為默認的話,你可轉到編譯器設置項,從預定義符號表中移除_MBCS
符號,同時添加上UNICODE和_UNICODE.(你應該把兩個都加上,因為不同頭文件使用不同符號.)但是,如
果你直接使用char作為字符串的話,將會遇到麻煩。
考慮以下代碼:
- HWND?hwnd?=?GetSomeWindowHandle();??
- ??
- char?szNewText[]?=?"we?love?Bob!";??
- ??
- SetWindowText?(?hwnd,?szNewText?);??
當編譯器將"SetWindowText"用"SetWindowTextW"替換后,代碼變為:
- HWND?hwnd?=?GetSomeWindowHandle();??
- ??
- char?szNewText[]?=?"we?love?Bob!";??
- ??
- SetWindowTextW?(?hwnd,?szNewText?);??
看到問題所在呢嗎?我們向需要Unicode字符串的函數傳遞了一個單字節字符串。解決這種問題的第一種方法就是在字符串變量定義的周圍使用#ifdef宏:
- HWND?hwnd?=?GetSomeWindowHandle();??
- ??
- #ifdef?UNICODE??
- ??
- wchar_t?szNewText[]?=?L"we?love?Bob!";??
- ??
- #else??
- ??
- char?szNewText[]?=?"we?love?Bob!";??
- ??
- #endif??
- ??
- SetWindowText?(?hwnd,?szNewText?);??
你肯定會為在每個字符串代碼周圍加上這些宏而頭疼不已。問題的解決方案就是使用TCHAR.
TCHAR?大救星
TCHAR是一種允許你為MBCS和Unicode應用使用同一分代碼的字符類型,它不需要在你整個代碼中寫這些零亂的#define宏。TCHAR的一種定義如下:
- #ifdef?UNICODE??
- typedef?wchar_t?TCHAR;??
- #else??
- typedef?char?TCHAR;??
- #endif??
因此一個TCHAR在MBCS工程中是char類型,在Unicode工程中是wchar_t類型。這里還有一個_T()宏,來處理Unicode字符串字面值所需的L前綴。
- <pre?name="code"?class="cpp"><pre?name="code"?class="cpp"><pre?name="code"?class="cpp"><pre?name="code"?class="cpp"><pre?name="code"?class="cpp"><pre?name="code"?class="cpp"><pre?name="code"?class="cpp"><pre?name="code"?class="cpp"><pre><pre?name="code"?class="cpp"><pre?name="code"?class="cpp"><pre?name="code"?class="cpp"><pre?name="code"?class="cpp"><pre>??
- <span?style="color:#000000;"></span><pre?name="code"?class="cpp">#ifdef?UNICODE??
- ??
- #define?_T(x)?L##x??
- ??
- #else??
- ??
- #define?_T(x)?x??
- ??
- #endif??
##是用來連接兩個參數的預編譯操作符。無論何時,在你代碼中有字符串字面值時,使用_T宏,那么在Unicode工程中就會添加上L前綴。
- <p></p><pre?name="code"?class="cpp"><pre?name="code"?class="cpp">TCHAR?szNewText[]?=?_T("we?love?Bob!");??
正如有隱藏SetWindowTextA/W的宏一樣,也有一些宏可以用來代替使用strxxx()?和_mbsxxx()字符串函數.例如,你可以使用_tcsrchr宏來替換strrchr()或者_mbsrchr或者wcsrchr._tcsrchr根據是否定義了_MBCS或者UNICODE符號而被展開為具體對應的函數,就像SetWindowText那樣。
不止strxxx()函數由TCHAR宏,還有很多,例如_stprintf()( 替換Sprintf()和swprintf() ),_tfopen()( 替換fopen()和_wfopen()? ).所有的宏定義列表在MSDN中"Generic-Text?Routine?Mappings"主題下可查.
String和TCHAR?typedef
由于Win32?API文檔以函數名列舉函數(l例如,"SetWindowText"),所有的字符串均以TCHAR形式給定。(例外之處是xp系統中的僅適用于Unicode的API)
下列是你可在MSDN中看到的常見typedef:
何時使用TCHAR和Unicode
那么講了這么多,你可能會想"為什么我要使用Unicode?我已經單單使用char好多年了"
下面三種情況使用Unicode將會頗有益處:
1.你的程序僅僅在Windows?NT系統上運行。
2.?你的程序要處理長度超過MAX_PATH的文件名。
3.你的程序使用了Windows?XP中新的API,而這些APi沒有區分的A/W版本。
大部分的Unicode?API都沒有在Windows?9x上執行,所以如果你只想你的程序在9x上運行,那你就要堅持使用MBCS?API.(微軟公司一些新的叫做MicroSoft?Layer的庫,允許在9X上使用Unicode?API,但是我沒有使用過,我不知道執行情況如何.)但是,既然NT系統內部所有的都采用Unicode,使用Unicode?API可以提高你程序運行的速度。每次你向MBCS?API傳遞一個字符串時,系統將字符串轉換為Unicode型,同時調用相應的Unicode?API。如果一個字符串返回了,那么操作系統將其轉換后再返回。盡管這些轉換操作都做了很大程度的優化來盡可能減小影響,但是鑒于其影響運行速度還是應該避免。
NT?只有在你使用Unicode?API時才允許使用超過MAX_PATH長度的文件名。使用Unicode?API?的好處一方面就是你的程序將自動處理不同用戶鍵入的任意語言。那么,當一個用戶可同時鍵入一個英文的、中文的、日文的文件名,而你可以不用編寫任何特別處理的代碼,因為它們對你而言都是Unicode字符。
最后,隨著Windows?9x的下線,微軟好像已經廢除了MBCS? API。例如,SetWindowTheme()?API,有兩個字符串參數,但是只有Unicode版本。使用Unicode工程將簡化你的字符串處理,因為你也不想在MBCS和Unicode之間來回轉換。
而且即使你現在沒有建立Unicode工程,你也應該一直使用TCHAR和相關的宏。
不僅僅因為這樣可以保證你代碼的DBCS安全性,同時當未來某個時候你想建立Unicode工程時,你只需改動一下你編譯器的設置!
原文地址:http://www.codeproject.com/Articles/2995/The-Complete-Guide-to-C-Strings-Part-I-Win32-Chara
下一部分原文地址:http://www.codeproject.com/Articles/3004/The-Complete-Guide-to-C-Strings-Part-II-String-Wra
下一部分譯文還在翻譯中...