原创 字元分類與字串處理收藏

新一篇: 更廣義的字元集轉換:iconv  | 旧一篇: UTF-16紹介

 

標準的 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 datacharmap、以及 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...)|编辑

新一篇: 更廣義的字元集轉換:iconv  | 旧一篇: UTF-16紹介

评论:没有评论。

发表评论  


当前用户设置只有注册用户才能发表评论。如果你没有登录,请点击登录
Csdn Blog version 3.1a
Copyright © Lafaer