標準的 ANSI C 函式庫提供了一系列的字元分類 (定義在 ctype.h) 與字串處理 (定義在 string.h) 函式、巨集來幫助程式設計者簡化字串處理的工作,相信已熟悉 C 語言的讀者對這些都不會感到陌生才對。由於這類函式都是用來處理單位元組字 (single byte character) 與其所構成的字串用的,故無法用在使用多位元組編碼系統的字元或字串之上。由於我們在處理多國語系的資料時,一樣需要做字元分類與字串處理,不過這裏我們所遇到的字元就不會只是傳統上常見的單位元組字了,而且在不同的 locale 下,編碼相同的字其代表的意義與其分類也不見得會一樣。故為了解決這個問題,在 I18N 架構下這些字元分類函式除了會隨著 locale LC_CTYPE 類別設定的不同而有不同的表現以外,ISO C89 同時還定義了另一組專門用來處理與分類寬字元的函式,讓我們可以在相同的原則下很方便地處理各國的字元。
我們先來簡單回顧一下原有的字元處理函式與巨集。在 ctype.h 的定義中,我們有以下的函式可用:
- int isalnum (int c): 判斷字元 c 是否為字母或數字。
- int isalpha (int c): 判斷字元 c 是否為字母。
- int isascii (int c): 判斷字元 c 是否為 ASCII 字元。
- int isblank (int c): 判斷字元 c 是否為空白鍵或 tab 字元, 此為 GNU extension。
- int iscntrl (int c): 判斷字元 c 是否為控制字元。
- int isdigit (int c): 判斷字元 c 是否為數字。
- int isgraph (int c): 判斷字元 c 是否為有筆畫的字元。
- int islower (int c): 判斷字元 c 是否為小寫字母。
- int isprint (int c): 判斷字元 c 是否為可印出的字元。
- int ispunct (int c): 判斷字元 c 是否為標點符號。
- int isspace (int c): 判斷字元 c 是否為空白字元,包括空白 鍵、tab 鍵、換行字元 ...等。
- int isupper (int c): 判斷字元 c 是否為大寫字母。
- int isxdigit (int c): 判斷字元 c 是否為 16 進位數字。
- int toupper (int c): 將字元 c 轉為大寫字母。
- int tolower (int c): 將字元 c 轉為小寫字母。
很明顯的,這樣的分類方式是針對傳統的 ASCII 碼而設計的,同時也適用於西方以字母拼音方式為主的語系。由於這些函式都是用來判斷單位元編碼的字元,故在使用此編碼的語系 (如歐美地區) 只要設好 locale,就能直接用這些函式來做字元處理。
然而,若遇到多位元編碼的字時會發生什麼樣的情況呢?就拿我們常用的 Big5 碼來說,我們寫一個如下的程式來測試看看:
#include <stdio.h>
#include <locale.h>
#include <ctype.h>
main()
{
unsigned char buf[100];
unsigned int slen, i, c;
setlocale(LC_CTYPE, "");
printf("Please input: ");
scanf("%[^\n]s", buf);
slen = strlen(buf);
for (i=0; i<slen; i++) {
c = (unsigned int)buf[i];
printf("Hex: 0x%02x: ", c);
if (isprint(c)) printf("%c: print ", (char)c);
if (isgraph(c)) printf("graph ");
if (isascii(c)) printf("ascii ");
if (isalpha(c)) printf("alpha ");
if (islower(c)) printf("lower ");
if (isupper(c)) printf("upper ");
if (isxdigit(c)) printf("xdigit ");
if (isspace(c)) printf("space ");
if (iscntrl(c)) printf("cntrl ");
printf("\n");
}
}
這個程式會先讀入一個由使用者輸入的字串,然後一個位元組一個位元組地來看在目前 locale 下是被歸類為那一個種字元。您可以試著在不同 locale 下執行並輸入同樣一段文字,看看會有什麼樣的效果。如果在
LC_CTYPE=C 的環境下執行此程式,然後在裏頭打入 Big5 中文字時,您會發現凡是
Hex 碼大於 127 的位元組都無法歸類,原因正是在此 locale 下其所用的字集編碼都只有在 ASCII 範圍內而已,而超過範圍的就沒有定義了。如果在
LC_CTYPE=zh_TW.Big5 的環境下執行並輸入 Big5 中文,則您會發現凡是先前無法歸類的那些位元組,現在都變成了可 print 且是 graph。因為這些位元組都是 Big5 中文字的一部分 (或第一個 byte、或第二個 byte),且每個 Big5 字都應該是可 print 且是 graph 的。
單純就每個位元組地來看待多位元組編碼的字串會造成嚴重的問題,也無法正確的判斷字元的屬性。以 Big5 為例,在字串中間任意取前後兩個位元組,就算這兩個位元組都是落在 Big5 碼定義的範圍,但它們兩個組合起來可不見得就會得出一個有意義的 Big5 字。舉個例子,Big5 的第一位元編碼範圍是 0xa1 - 0xf9,第二位元範圍是 0x40 - 0x7e 與 0xa1 - 0xfe,則 0xa8 這個位元組符合 Big5 第一與第二位元的範圍,而 0x48 只符合 Big5 的第二位元的範圍,則 0xa8 0x48 這樣的組合有意義,也就是 ``沈'' 這個字,但反過來 0x48 0xa8 這樣的組合就沒有意義了,因為它已違反了 Big5 的編碼規則。然而,不論 0xa8 與 0x48 這兩個位元怎麼排,既然它們都落在 Big5 第一或第二位元的範圍,在上頭的範例程式來看都一定是可 print 且 graph。而且,對於有特別歸類的字如 Big5 的全形英文字 ``A'',使用上述的函式我們也無從得知由 0xa2 與 0xcf 組成的 ``A'' 字是屬於 alpha 且屬於 xdigit。
因此,上述 ctype.h 所定義的巨集,並不適合拿來做 mb (多位元組) 字的分類與處理。事實上我們也無法直接做到多位元組字串的處理,必須先把 它們轉成寬字元 (wc) 之後再來處理才行,ISO C89 中另外引入了專門用來做寬字元分類與處理的一組函式巨集,這些函式在 glibc 裏頭是定義在 wctype.h 裡面,它們的基本形為 wctype() 與 iswctype() 兩函式,其函式宣告如下:
wctype_t wctype (const char *PROPERTY);
int iswctype (wint_t WC, wctype_t DESC);
其中
wctype() 函式由
PROPERTY 字串中輸入一個字元分類名稱,並將代表此分類名稱的值經由
wctype_t 形別傳回,而此傳回的值可進一步再傳入
iswctype() 函式中,用以判斷字元
WC 是歸類於那一類字元。這樣說讀者也許會覺得模糊,且看以下的例子:
int iswc_alnum(wint_t wc)
{
return iswctype (wc, wctype("alnum"));
}
在這裏我們寫了一個用來判斷
wc 這個寬字元是否為字母或數字的函式,是的話就傳回非 0 的值。讀者可以見到在這裏我們將字串 ``alnum'' 傳入
wctype() 中,藉此告訴
iswctype() 函式說,我們所要做的判斷目標為 ``是否為字母或數字''。因此,只要輸入適當的
PROPERTY 字串,如上的
iswctype() 與
wctype() 兩函式的組合就可以做出所有字元分類的判斷。這類可以做為
PROPERTY 輸入的字串計有
| "alnum" |
"alpha" |
"blank" |
"cntrl" |
| "digit" |
"graph" |
"lower" |
"print" |
| "punct" |
"space" |
"upper" |
"xdigit" |
各位讀者可以回去對照一下先前所提到的
ctype.h 所定義的函式群,就會見到除了
isascii() 以外,各
is*() 函式與這邊的
PROPERTY 字串都有一對一的對應關係。
在這裏我們見到了一個新的形別:wint_t。就好像在 ctype.h 裏所定義的巨集函式中,每個 char 字是以 int 做為引數形別傳入巨集一樣,在這裏每個 wchar_t 字都是以 wint_t 形別的引數傳入函式巨集的。
除此之外,有幾個定義在 wchar.h 中的巨集是與 wint_t 有直接關係的,在這裏也一併提一下。首先是 wint_t WCHAR_MIN 與 wint_t WCHAR_MAX,分別代表一個 wint_t 變數所能表達的最大與最小值。另外還有一個 wint_t WEOF,意思同等於我們過去常見的 EOF,但要注意的是,WEOF 與 EOF 的值不見得相同,也不見得就小於 1,故在某些特殊場合的使用上要小心留意。
然而,在一般的應用上,也許我們更常使用以下的函式群,來代替直接使用 wctype() 與 iswctype() 兩函式;
| int iswalnum (wint_t wc) |
|
int iswalpha (wint_t wc) |
| int iswblank (wint_t wc) |
|
int iswcntrl (wint_t wc) |
| int iswdigit (wint_t wc) |
|
int iswgraph (wint_t wc) |
| int iswlower (wint_t wc) |
|
int iswprint (wint_t wc) |
| int iswpunct (wint_t wc) |
|
int iswspace (wint_t wc) |
| int iswupper (wint_t wc) |
|
int iswxdigit(wint_t wc) |
各位可以看得出來,這些
isw*() 函式與先前所提的
is*() 函式巨集有相當明確的對應關係,而且用法也完全一樣。事實上,這些函式都是以
wctype() 與
iswctype() 為基礎而實作出來的。而我們先前所寫下的那個範例函式
iswc_alnum(),其功能就全等於這裏的
iswalnum() 函式。
相對於 ctype.h 中所定義的 toupper() 與 tolower(),在 wctype.h 中同樣也有定義用來轉換寬字元字母大小寫的函式。如同先前所提,這類的轉換函式也是有兩個基本形:
wctrans_t wctrans (const char *PROPERTY);
wint_t towctrans (wint_t WC, wctrans_t DESC);
其中
wctrans() 可接受的
PROPERTY 字串只有 ``tolower'' 與 ``woupper'' 兩個。而根據這兩個基本形函式所實作出來的即為如下兩個函式:
| int towupper (wint_t wc) |
|
int towlower (wint_t wc) |
因此,使用這些寬字元分類處理函式,我們就可以將先前那個範例程式改寫如下:
#include <stdio.h>
#include <locale.h>
#include <wctype.h>
#include <wchar.h>
main()
{
unsigned char buf[100], *s;
unsigned int slen, i;
mbstate_t state;
wchar_t wbuf[100];
wint_t c;
setlocale(LC_CTYPE, "");
printf("Please input: ");
scanf("%[^\n]s", buf);
s = buf;
memset(&state, '\0', sizeof(mbstate_t));
if (mbsrtowcs(wbuf, (const char **)&s, 100, &state) ==
(size_t)-1) {
printf("mbsrtowcs convert error.\n");
return;
}
slen = wcslen(wbuf);
for (i=0; i<slen; i++) {
c = (wint_t)wbuf[i];
printf("Hex: 0x%04x: ", (unsigned int)c);
if (iswprint(c)) printf("print ");
if (iswgraph(c)) printf("graph ");
if (iswalpha(c)) printf("alpha ");
if (iswlower(c)) printf("lower ");
if (iswupper(c)) printf("upper ");
if (iswxdigit(c)) printf("xdigit ");
if (iswspace(c)) printf("space ");
if (iswcntrl(c)) printf("cntrl ");
printf("\n");
}
}
在這裏,程式將字串讀入後,就轉換成 wcs 的形式,然而再逐一檢視每一個 wc 字的類別。在這裏我們用了一個
wcslen() 函式來計算 wcs 字串的長度 (即總共有多少個 wc 字),它的功用就與
strlen() 一樣,後頭我們會再做介紹。
理論上,經過這樣改進之後,我們的程式就可以在任何 locale 下正確地做到字元分類與處理的工作了。事實上在 glibc-2.1 系統下的測試結果,不論是在 C 或 zh_TW.Big5 locale 下,若是輸入純 ASCII 字元時都不會有問題。但不幸的是,若在 zh_TW.Big5 locale 下輸入 Big5 中文時,這個程式仍然無法做到正確的字元分類工作,這問題在那裏呢?我們認為,問題的徵結就在我們之前提過的,glibc-2.1.x 系列的 I18N 與 locale 的實作還沒有到百分之百的地步,特別是對我們東方使用多位元組編碼字集的語系而言。
記得我們先前在談由 locale data、charmap、以及 repertoire map 來產生我們的 zh_TW.Big5 locale 時有特別提到,要由 localedef 來編譯 locale data 時,一定要指定一個 repertoire map 檔,而這個檔的作用就是要做該 locale 所採用的字集中,每個「字」的符號與系統「基底字集」編碼的對應。由於在目前 glibc-2.1.x 系統中,這個 repertoire map 尚未完工,也就是它目前只能包含單位元字的部分,而無法包含多位元組字,這也就是為什麼在我們先前的測試中,所輸入的 ASCII 碼可以正確無誤,但 Big5 中文字就不行的原因。換句話說,整個 wctype.h 所定義的 wc 字分類處理函式,其工作機制就是完全以 repertoire map 的內容為基礎。因此,我們期待將來 glibc-2.2 完成後,可以完全解決這個問題。
到目前為止,我們已見到了在很多情況下,我們都必須將原有的 mbs 字串轉成 wcs 字串後,以方便後續的處理。然而,如果我們需要做字串操作,如字串拷備、字串銜接、字串比較 ...等工作時,我們是不是也要先將 wcs 字串轉 mbs 形式,然後再用 strcpy()、strcat()、與 strcmp 等函式來操作呢?不需要這麼麻煩,因為在 ISO C89 已有定義一組 wcs 字串處理函式,宣告在 wchar.h 中,與我們常見的 str*() 完全一對一對應,用法也完全一樣。它們的函式名都是以 wcs*() 開頭的,而所輸入的字串引數形別都是 wchar_t *,像先前我們見到的 wcslen() 就是一例。除此之外,還有一些以 wmem*() 開頭的函式,即對應於 mem*() 陣列處理函式。這些函式包括:
| wcscmp() |
|
wcscasecmp() |
|
wcsncmp() |
|
wcsncasecmp() |
| wcscpy() |
|
wcsncpy() |
|
wcscat() |
|
wcsncat() |
| wcsdup() |
|
wcslen() |
|
wcstok() |
|
wcsstr() |
| ... |
|
... |
|
... |
|
... |
| wmemcmp() |
|
wmemcpy() |
|
wmemmove() |
|
wmemset() |
| ... |
|
... |
|
... |
|
... |
相信對於已熟悉
char * 字串處理函式的讀者們,見到這些處理
wchar_t * 函式應該可以直接顧名思義,故在這裏我們就不多花篇幅一一介紹了。
发表于 @ 2004年12月04日 13:17:00|评论(loading...)|编辑