Unicode 字符集學習筆記

字符集

軟件的本地化要解決的真正問題,實際上就是如何來處理不同的字符集。多年來,許多人一直將文本串作爲一系列單字節字符來進行編碼,並在結尾處放上一個零。對於我們來說,這已經成了習慣。當調用s t r l e n函數時,它在以0結尾的單字節字符數組中返回字符的數目。
問題是,有些文字和書寫規則(比如日文中的漢字就是個典型的例子)的字符集中的符號太多了,因此單字節(它提供的符號最多不能超過 2 5 6個)是根本不敷使用的。爲此出現了雙字節字符集( D B C S),以支持這些文字和書寫規則。

單字節與雙字節字符集

在雙字節字符集中,字符串中的每個字符可以包含一個字節或包含兩個字節。例如,日文中的漢字,如果第一個字符在 0 x 8 1與0 x 9 F之間,或者在0 x E 0與0 x F C之間,那麼就必須觀察下一個字節,才能確定字符串中的這個完整的字符。使用雙字節字符集,對於程序員來說簡直是個很大的難題,因爲有些字符只有一個字節寬,而有些字符則是兩個字節寬。
如果只是調用 s t r l e n函數,那麼你無法真正瞭解字符串中究竟有多少字符,它只能告訴你到達結尾的0之前有多少個字節。 A N S I的C運行期庫中沒有配備相應的函數,使你能夠對雙字節字符集進行操作。但是, Microsoft Visual C++的運行期庫卻包含許多函數,如 _ m b s l e n ,它可以用來操作多字節(既包括單字節也包括雙字節)字符串。
爲了幫助你對D B C S字符串進行操作, Wi n d o w s提供了下面的一組幫助函數(見表 2 - 1 )。前兩個函數CharNext 和Char Prev 允許前向或逆向遍歷DBCS 字符串,方法是每次一個字符。第三個函數sDBCSLeadByte, 在字節返回到一個兩字字節符的第一個字節時將返回T R U E。

Unicode:寬字節字符集

U n i c o d e提供了一種簡單而又一致的表示字符串的方法。 U n i c o d e字符串中的所有字符都是1 6位的(兩個字節)。它沒有專門的字節來指明下一個字節是屬於同一個字符的組成部分,還是一個新字符。這意味着你只需要對指針進行遞增或遞減,就可以遍歷字符串中的各個字符,不再需要調用C h a r N e x t、 C h a r P r e v和I s D B C S L e a d B y t e之類的函數。

如何編寫U n i c o d e源代碼

M i c r o s o f t公司爲U n i c o d e設計了Windows API,這樣,可以儘量減少對你的代碼的影響。實際上,你可以編寫單個源代碼文件,以便使用或者不使用 U n i c o d e來對它進行編譯。只需要定義兩個宏( U N I C O D E和_ U N I C O D E),就可以修改然後重新編譯該源文件。

C運行期庫對U n i c o d e的支持

爲了利用 U n i c o d e字符串,定義了一些數據類型。標準的 C頭文件S t r i n g . h已經作了修改,以便定義一個名字爲w c h a r _ t的數據類型,它是一個U n i c o d e字符的數據類型:
typedef unsigned short wchar_t;

例如,如果想要創建一個緩存,用於存放最多爲 9 9個字符的U n i c o d e字符串和一個結尾爲零的字符,可以使用下面這個語句:
wchar_t szbuffer[100];

該語句創建了一個由 1 0 0個1 6位值組成的數組。當然,標準的 C運行期字符串函數,如s t r c p y、 s t r c h r和s t r c a t等,只能對A N S I字符串進行操作,不能正確地處理U n i c o d e字符串。因此,ANSI C也擁有一組補充函數。清單2 - 1顯示了一些標準的ANSI C字符串函數,後面是它們的等價U n i c o d e函數。
char strcat(char , const char *);
wchar_t wcscat(wchar_t *, wchar_t);

int strchr(const char *, int);
wchar_t wcschr(const wchar_t *, const wchar_t);

int strcmp(char , const char );
wchar_t wcscmp(wchar_t , const wchar_t );

size_t strlen(const char *);
size_t wcslen(const wchar_t *);
標準的ANSI C字符串函數和它們的等價U n i c o d e函數

請注意,所有的U n i c o d e函數均以w c s開頭, w c s是寬字符串的英文縮寫。若要調用 U n i c o d e函數,只需用前綴w c s來取代 A N S I字符串函數的前綴 s t r即可。

注意 大多數軟件開發人員可能已經不記得這樣一個非常重要的問題了,那就是M i c r o s o f t公司提供的 C運行期庫與A N S I的標準C運行期庫是一致的。 ANSI C規定, C運行期庫支持U n i c o d e字符和字符串。這意味着始終都可以調用 C運行期函數,以便對U n i c o d e字符和字符串進行操作,即使是在Windows 98上運行,也可以調用這些函數。換句話說, w c s c a t、 w c s l e n和w c s t o k等函數都能夠在Windows 98上很好地運行,這些都是必須關心的操作系統函數。

對於包含了對s t r函數或w c s函數進行顯式調用的代碼來說,無法非常容易地同時爲 A N S I和U n i c o d e對這些代碼進行編譯。本章前面說過,可以創建同時爲 A N S I和U n i c o d e進行編譯的單個源代碼文件。若要建立雙重功能,必須包含 T C h a r. h文件,而不是包含S t r i n g . h文件。T C h a r. h文件的唯一作用是幫助創建 A N S I / U n i c o d e通用源代碼文件。它包含你應該用在源代碼中的一組宏,而不應該直接調用 s t r函數或者 w c s函數。如果在編譯源代碼文件時定義了_ U N I C O D E,這些宏就會引用w c s這組函數。如果沒有定義 _ U N I C O D E,那麼這些宏將引用 s t r這組宏。
例如,在T C h a r. h中有一個宏稱爲_ t c s c p y。如果在包含該頭文件時沒有定義 _ U N I C O D E ,那麼_ t c s c p y就會擴展爲A N S I的s t r c p y函數。但是如果定義了_UNICODE, _tcscpy將擴展爲U n i c o d e的w c s c p y函數。擁有字符串參數的所有 C運行期函數都在T C h a r. h文件中定義了一個通用宏。如果使用通用宏,而不是A N S I / U n i c o d e的特定函數名,就能夠順利地創建可以爲 A N S I或U n i c o d e進行編譯的源代碼。
但是,除了使用這些宏之外,還有一些操作是必須進行的。 T C h a r. h文件包含了另外一些宏。若要定義一個A N S I / U n i c o d e通用的字符串數組,請使用下面的 T C H A R數據類型。如果定義了_ U N I C O D E, T C H A R將聲明爲下面的形式:
typedef wchar_t TCHAR;
如果沒有定義_ U N I C O D E,則T C H A R將聲明爲下面的形式:
typedef char TCHAR;
使用該數據類型,可以像下面這樣分配一個字符串:
TCHAR szString[100];
也可以創建對字符串的指針:
TCHAR *szError = “Error”;
不過上面這行代碼存在一個問題。按照默認設置, M i c r o s o f t公司的C + +編譯器能夠編譯所有的字符串,就像它們是 A N S I 字符串,而不是 U n i c o d e 字符串。因此,如果沒有定義_ U N I C O D E,該編譯器將能正確地編譯這一行代碼。但是,如果定義了 _ U N I C O D E,就會產生一個錯誤。若要生成一個 U n i c o d e字符串而不是A N S I字符串,必須將該代碼行改寫爲下面的樣子:
TCHAR *szError = L”Error”;
字符串( literal string)前面的大寫字母L,用於告訴編譯器該字符串應該作爲 U n i c o d e字符串來編譯。當編譯器將字符串置於程序的數據部分中時,它在每個字符之間分散插入零字節。這種變更帶來的問題是,現在只有當定義了 _ U N I C O D E時,程序才能成功地進行編譯。我們需要另一個宏,以便有選擇地在字符串的前面加上大寫字母 L。這項工作由 _ T E X T宏來完成,_ T E X T宏也在T C h a r. h文件中做了定義。如果定義了 _ U N I C O D E,那麼_ T E X T定義爲下面的形式:

#define _TEXT(x) L##x
如果沒有定義_ U N I C O D E, _ T E X T將定義爲:
#define _TEXT(x) x
使用該宏,可以改寫上面這行代碼,這樣,無論是否定義了 _ U N I C O D E宏,它都能夠正確地進行編譯。如下所示:
TCHAR *szError = _TEXT(“Error”)

_ T E X T宏也可以用於字符串。例如,若要檢查一個字符串的第一個字符是否是大寫字母 J,
只需編寫下面的代碼即可:

if (szError[0] == _TEXT(“J”))
{
//first char is J
}
else
{
//first char is not J
}

Wi n d o w s頭文件定義了表2 - 3列出的數據類型。

                            表2-3 Uincode 數據類型 
數 據 類 型                                                       說 明 
W C H A R                                                   U n i c o d e字符 
P W S T R                                               指向U n i c o d e字符串的指針
P C W S T R                                     指向一個恆定的U n i c o d e字符串的指針

這些數據類型是指 U n i c o d e字符和字符串。 Wi n d o w s頭文件也定義了 A N S I / U n i c o d e通用數
據類型P T S T R和P C T S T R。這些數據類型既可以指 A N S I字符串,也可以指 U n i c o d e字符串,這
取決於當編譯程序模塊時是否定義了 U N I C O D E宏。

請注意,這裏的 U N I C O D E宏沒有前置的下劃線。 _ U N I C O D E宏用於C運行期頭文件,而U N I C O D E宏則用於Wi n d o w s頭文件。當編譯源代碼模塊時,通常必須同時定義這兩個宏。

成爲符合A N S I和U n i c o d e的應用程序

即使你不打算立即使用U n i c o d e,最好也應該着手將你的應用程序轉換成符合 U n i c o d e的應用程序。下面是應該遵循的一些基本原則:
• 將文本串視爲字符數組,而不是c h a r s數組或字節數組。
• 將通用數據類型(如T C H A R和P T S T R)用於文本字符和字符串。
• 將顯式數據類型(如B Y T E和P B Y T E)用於字節、字節指針和數據緩存。
• 將T E X T宏用於原義字符和字符串。
• 執行全局性替換(例如用P T S T R替換P S T R)。
• 修改字符串運算問題。例如函數通常希望你在字符中傳遞一個緩存的大小,而不是字節。
這意味着你不應該傳遞 s i z e o f ( s z B u ff e r ) ,而應該傳遞( s i z e o f ( s z B u ff e r ) / s i z e o f ( T C H A R )。另外,如果需要爲字符串分配一個內存塊,並且擁有該字符串中的字符數目,那麼請記住要按字節來分配內存。這就是說,應該調用 malloc(nCharacters *sizeof(TCHAR)), 而不是調用 m a l l o c( n C h a r a c t e r s )。在上面所說的所有原則中,這是最難記住的一條原則,如果操作錯誤,編譯器將不發出任何警告。

在U n i c o d e與A N S I之間轉換字符串

Wi n d o w s函數M u l t i B y t e To Wi d e C h a r用於將多字節字符串轉換成寬字符串。下面顯示了M u l t i B y t e To Wi d e C h a r函數。
int MultiByteToWideChar(
UINT CodePage,
DWORD dwFlags,
LPCSTR lpMultiByteStr,
int cchMultiByte,
LPWSTR lpWideCharStr,
int cchWideChar
);
u C o d e P a g e參數用於標識一個與多字節字符串相關的代碼頁號。 d w F l a g s參數用於設定另一個控件,它可以用重音符號之類的區分標記來影響字符。這些標誌通常並不使用,在 d w F l a g s參數中傳遞0。 p M u l t i B y t e S t r參數用於設定要轉換的字符串, c c h M u l t i B y t e參數用於指明該字符串的長度(按字節計算)。如果爲c c h M u l t i B y t e參數傳遞- 1,那麼該函數用於確定源字符串的長度。
轉換後產生的 U n i c o d e版本字符串將被寫入內存中的緩存,其地址由 p Wi d e C h a r S t r參數指定。必須在 c c h Wi d e C h a r 參數中設定該緩存的最大值(以字符爲計量單位) 。如果調用M u l t i B y t e To Wi d e C h a r,給c c h Wi d e C h a r參數傳遞0,那麼該參數將不執行字符串的轉換,而是返回爲使轉換取得成功所需要的緩存的值。一般來說,可以通過下列步驟將多字節字符串轉換成U n i c o d e等價字符串:
1) 調用M u l t i B y t e To Wi d e C h a r函數,爲p Wi d e C h a r S t r參數傳遞N U L L,爲c c h Wi d e C h a r參數
傳遞0。
2) 分配足夠的內存塊,用於存放轉換後的 U n i c o d e字符串。該內存塊的大小由前面對M u l t B y t e To Wi d e C h a r的調用返回。
3) 再次調用M u l t i B y t e To Wi d e C h a r,這次將緩存的地址作爲 p Wi d e C h a r S t r參數來傳遞,並傳遞第一次調用M u l t i B y t e To Wi d e C h a r時返回的緩存大小,作爲c c h Wi d e c h a r參數。
4. 使用轉換後的字符串。
5) 釋放U n i c o d e字符串佔用的內存塊。
函數Wi d e C h a r To M u l t i B y t e將寬字符串轉換成等價的多字節字符串,如下所示:
int WideCharToMultiByte(
UINT CodePage, //指定執行轉換的代碼頁
DWORD dwFlags, //允許你進行額外的控制,它會影響使用了讀音符號(比如重音)的字符
LPCWSTR lpWideCharStr, //指定要轉換爲寬字節字符串的緩衝區
int cchWideChar, //指定由參數lpWideCharStr指向的緩衝區的字符個數
LPSTR lpMultiByteStr, //指向接收被轉換字符串的緩衝區
int cchMultiByte, //指定由參數lpMultiByteStr指向的緩衝區最大值
LPCSTR lpDefaultChar, //遇到一個不能轉換的寬字符,函數便會使用pDefaultChar參數指向的字符
LPBOOL pfUsedDefaultChar //至少有一個字符不能轉換爲其多字節形式,函數就會把這個變量設爲TRUE
);
該函數與M u l t i B i t e To Wi d e C h a r函數相似。同樣, u C o d e P a g e參數用於標識與新轉換的字符串相關的代碼頁。 d w F l a g s則設定用於轉換的其他控件。這些標誌能夠作用於帶有區分符號的字符和系統不能轉換的字符。通常不需要爲字符串的轉換而擁有這種程度的控制手段,你將爲d w F l a g s參數傳遞0。p Wi d e C h a r S t r參數用於設定要轉換的字符串的內存地址, c c h Wi d e C h a r參數用於指明該字符串的長度(用字符數來計量)。如果你爲c c h Wi d e C h a r參數傳遞- 1,那麼該函數用於確定源字符串的長度。
轉換產生的多字節版本的字符串被寫入由p M u l t i B y t e S t r參數指明的緩存。必須在c c h M u l t i B y t e參數中設定該緩存的最大值(用字節來計量)。如果傳遞 0作爲Wi d e C h a r To M u l t i B y t e函數的
c c h M u l t i B y t e參數,那麼該函數將返回目標緩存需要的大小值。通常可以使用將多字節字符串轉換成寬字節字符串時介紹的一系列類似的事件,將寬字節字符串轉換成多字節字符串。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章