2013年4月9日 星期二

Visual Studio 的 Unicode 概念

來源:http://www.csie.nctu.edu.tw/~skyang/unicode.zhtw.htm

[轉貼] 萬國碼的概念與專案
萬國碼的概念與專案

甚麼是萬國碼(Unicode)

Unicode 是一套試圖定義全世界文字所用內碼的內碼表,發展的緣由還是字碼的問題,全美國的文字代碼用 ASCII 統一了,那麼國與國之間呢?例如台灣用 Big5 碼,中國大陸用 GB 碼...。自從網際網路開始發達起來,透過網路接觸到不同國家的軟體與內容(content)的機會就變多了,如何讓使用不同國家文字的內容都在同一台電腦上正常顯示呢。

在前篇提過的ASCII碼是由美國國家標準學會所制訂的,供全美國的電腦通訊交換資料的標準內碼,是一種官方標準,而是 Unicode 則是由本部位於加州的國際民間組織制訂的非政府官方標準,主要成員有 Adobe , Apple, HP, IBM, Microsoft, Xerox...自 1991 年發佈第一版以來,至今已經超過十萬字,而且仍在擴充中。

既然目前 unicode 已經超過了 10 萬字,所以必須使用 32 bits 才足夠表示一個 unicode 字元,事實上目前有 UTF-8、UTF-16、UTF-32 版本的 unicode 內碼表,其中 UTF-16 以 16 bits 來儲存一個 unicode 的字元,而 UTF-32 即是以 32 bits 來儲存 unicode 字元,所以 UTF-16 只能儲存一些 unicode 早期的常用字元、例如游錫堃的堃就不在 UTF-16 字碼表中,目前繁體中文在 unicode 裡有 14758 個字,然而 UTF-8 可不是只用 8 bits 來儲存 uncide 字元喔,這是後來發展出的可變長度編碼法,請容後介紹。

電腦中的 Unicode

自從電腦會上網以後,你有很多機會接觸外國軟體,所以 Windows 自 Windows 2000 以後,所有的系統顯示文字內容都以 unicode (UTF-16)為內碼,換言之,你在開始功能表、檔案總管,內附軟體看到的每一個字,都是用 16-bit 的 unicode 字元儲存的, 而Windows內部的文字顯示軟體元件也都是專為顯示 unicode 文字內容而設計,也因此,在 Windows 程式設計中提到的 unicode 或寬字元(wide char)都是指 UTF-16 內碼的字元。

看到這裡你可能會有個問題:既然 Windows用的 unicode 是涵蓋全世界各語言文字的內碼,難道現在的 Windows 電腦都有灌全世界文字的字型?並沒有!所以遇到缺字型的文字,會要求你安裝語言包,然後就可以正常顯示那些異國文字了。

字碼頁(code page)

然而,並不是所有的軟體都已經以 unicode 為內碼,許多軟體還是以 ASCII 或是各國家獨特的內碼顯示的,如果這些舊軟體無法在新版本的 Windows 上執行會造成很大的困擾,但是又無法去強制這些軟體改版的情況下,Windows 採用了一種稱為字碼頁的過渡性機制。

字碼頁(code page)其實是各種內碼轉換到 Unicode的轉碼表,例如台灣用的繁體中文(big-5)就是編號為 950 的字碼頁,當應用程式以非 unicode 的輸出函式、輸出了非 unicode 的文字的時候,Windows會依照系統目前使用中的字碼頁(通常就是各國版本Windows已經預設的字碼頁),將這些文字轉換為 unicode 再輸出之,所以 Windows 在執行這些非 unicode 的程式的時候,事實上是做了內碼轉換才顯示它們的文字的。

除了螢幕上的文字顯示以外,其實檔案存取也是,特別是我們俗稱 ASCII 文字檔的純文字 (plain text)檔案,例如 .txt 檔案,這些檔案本身並不像 .htm 或 .doc 夾帶有邊碼資訊(.htm 的檔頭中告知了這個網頁是採用何種編碼),所以會被系統視為採用各國自己的編碼,所以要把文字從檔案中讀取到 unicode 程式當中的時候,其實也透過了字碼頁將它們轉換為 unicode。

Unicode 應用程式

這麼說來,應用程式似乎分為 unicode 應用程式和非 unicode 應用程式兩種,那麼是否 unicode 程式當中所有的文字就必須採用 unicode 內碼呢?其實不是的,若真是如此的話,那麼所有的 unicode 應用程式都不能採用非 unicode 編碼的舊程式庫(或是由其他平台例如 Linux 移植來的程式庫),那麼麻煩就大了。

其實俗稱的 uincode 應用程式和非 unicode 應用程式,並不是產生的機械碼或撰寫程式時的專案類型有何不同,它們都是「普通的應用程式」,而是 Windows 程式庫當中,所有的文字輸出入、檔案文字輸出入、字串處理函式本來就有 unicode 版本和非 unicode 版本兩套,而俗稱的 unicode 程式則是全面地採用 unicode 版本的輸出入函式,所以是程式寫作技巧的差別而不是軟體本身的差別。

在前篇已經提到 char 類別是 8 bits、以 ASCII 碼做為文字內碼的字元型別,類似地,在 C/C++ 當中另有 16 bits 的 wchar_t 型別用來儲存 16 bits 的 unicode 字元(UTF-16 字元),這種字元在 Windows 程式設計當中也稱為寬字元(wide char), 而採用 char * 字串的處理函式、例如 strcpy( )、也都另有 wchat_t * 版本,例如 wcscpy( ),兩套函式是可以在同一個程式中並用的。

如前所述,俗稱的 unicode 應用程式在程式碼當中,全面地以 wchat_t 代替 char、以 wchat_t 版本的函式來代替 char 版本的函式,例如 strcpy( ) 換成 wcscpy( )、printf( ) 換成 wprintf( )、main( ) 換成 wmain( ),所以這程式輸出入的所有文字內容都是採用 unicode 內碼。以 Visual Studio 來說,如果你有此打算而在新建專案類型的時候勾選了「使用 unicode 程式庫」,它會在建立專案的時候預先為你做好一些額外的初始化動作,所以在主控台程式當中,會以 wmain( ) 來代替 main( ),請見以下的 UniHelloWorld 範例。
#include <stdio.h>

void wmain(void)
{
    wchar_t str[16] = L"Hello World!";
    wprintf(L"%s\n", str);

    char strAsc[16] = "Hello World!";
    printf("%s\n", strAsc);
}
如本例所示,unicode 字元與字串乃以 wchar_t 型別儲存,其實 wchar_t 就是 short int,而在撰寫程式的時候,unicode 字串在 "" 之前加上 L,就指定了此字串在編譯的時候要以 unicode 來儲存,同理,unicode 字元在 ' '前加上L,例如 L ' a'。

在這個例子當中你看到了 unicode 和非 unicode 的字串和函式是可以並存的,此程式的執行結果很單純地如下所示,就是兩行 Hello World! 看似沒有差異,但其實下行的 printf( ) 述句乃是以 ASCII 碼輸出文字的,所以在 Windows 內部其實會經過字碼頁的對應轉換,然後才顯示出來的。

Hello World!
Hello World!

當你在 Visual Studio 的方案總管,在這個 UniHelloWorld 專案上按右鍵\屬性,可以看到字元集選項有使用 unicode 字元集,勾選了以後表示這個專案會使用到 unicode 函式,而且程式中會多出一個 _UNICODE 定義。

如何在程式碼當中得知現在的程式是不是使用 Unicode 的關鍵就是這個 _UNICODE 定義,例如在 UniHelloWorld.cpp 中你可以再加上這樣的程式碼:

#ifdef _UNICODE
wprintf(L"I am using Unicode!\n");
#endif
你可以試著在專案屬性中把字元集改為「使用多位元組字元集」,UniHelloWorld.cpp 就不會顯示這行文字,在 Windows 中 ASCII 也算一種多位元組字元集。


那麼我們要自問:我寫程式的時候應該盡量使用 unicode 字元及其函式嗎?如果你是撰寫 Windows 程式的話,這個答案是 Yes,因為現在的 Windows 都是以 unicode為內碼,你的程式輸出非 unicode 碼都會透過字碼頁轉換為 unicode 再顯示,例如上例中的 printf("Hello World!"); 故以效率起見,應該全面使用 unicode 函式,不過若是其他的平台就無所謂了,因為其他的平台不一定以 unicode 做為系統內碼,甚至可能根本不支援 unicode。

甚麼是 UTF-8

在 Windows 程式當中的 unicode 就是 UTF-16,也就是每個 unicode 字元都用 16 bits 儲存,所以是不太有機會使用到 UTF-8 的,不過在此還是介紹一下。如前所述,在 Windows 當中既然採用了 16 bits 的 UTF-16 內碼,當然所有文字所需的儲存空間都會是 8 bits 的 ASCII 碼的兩倍,就算這些文字內容只是英文與數字構成的也是如此,以目前電腦儲存設備如此便宜的情況下,這點並不是問題,不過若以英文為主的網頁也都以 UTF-16 儲存,導致網路傳輸量都變成兩倍,那問題就很大了,所以發展出了 UTF-8 編碼法(UTF-8 encoding),簡而言之,UTF-8是一種可變長度的字元編碼,前 7-bits 與 ASCII 相同,第 8 個 bit 若為1,後續內碼可能 8~32 bits 不等。

UTF-8 的優點就是和 ASCII 相容,所以舊有的 ASCII 文件或網頁不經任何轉碼,就可以在採用 UTF-8 內碼的閱讀器或瀏覽器上閱讀,當然,UTF-8 的優點就是 UTF-16 的缺點,UTF-16的缺點就是和ASCII不相容,所有的 ASCII 文字內容必須經過轉碼才能夠在 UTF-16 的閱讀器或瀏覽器上閱讀,那麼為甚麼不要連 Windows 都全面採用 UTF-8 呢?因為 UTF-8 是一種「可變長度」的編碼,所以你無法由字數來推斷字串所需的 空間 bytes,這對程式設計來說是非常不便的事情,以程式設計來說,如果你採用 ASCII 內碼,那麼你要儲存 n 個字元的空間就是 n bytes,採用 UTF-16 的話就是 2n bytes,非常容易推斷,但採用 UTF-8 的話就從 n~4n bytes 都有可能,所以到目前為止的 Windows 版本尚不以 UTF-8 為內碼。

本篇重點回顧

您知道了嗎?
Unicode 是一種(試圖)涵蓋全世界的電腦內碼。
Windows已經全面以 UTF-16 做為內碼。
網頁用的 UTF-8 是與 ASCII 相容的一種Unicode內碼。
程式中以 wchar_t 和 wide char 系列函式處理 Unicode。
Unicode程式專案有 _UNICODE 定義。


來源:http://www.csie.nctu.edu.tw/~skyang/unicodestr.zhtw.htm

[轉貼] 萬國碼字串函式
Unicode 字元與字串

在 C/C++語言當中可以以前綴字 L 告知編譯器這是個 Unicode 字元或字串,於是編譯器便會將這些文字以 UTF-16 內碼存放。Unicode 字元的型別是 wchar_t,例如:

wchar_t ch = L'a';
就是宣告了一個 unicode 字元 ch 而且令它為字母 a,須注意的是 L'a' 不等於 'a',也不等於 97,因為 'a' 是 char 型別的字母 a,乃是以 ASCII 碼表示的,而字母 a 的 ASCII 碼是 97,所以 'a' 等於 97,然而 unicode 字元 L'a' 乃是以 UTF-16 內碼來表示字母 a,UTF-16 中的字母 a 不是 97,所以 L'a' 自然也不等於 'a'。

既然 unicode 字元是以 wchar_t 儲存的,unicode 字串自然也是以 wchar_t 陣列來存放,例如:

wchar_t myname[8] = L"SKY";
這個字串是個以 L'\0' 為結束字元的字串,不是 ASCII 的 '\0' 喔。

Unicode 字串函式

如前篇所述,unicode 字元及字串另有一套處理與輸出入函式,同樣是 #include ,例如字串處理函式有:

Unicode版本ASCII版本
區分大小寫的比較int wcscmp( const wchar_t *string1, const wchar_t *string2);int strcmp( const char *string1, const char *string2);
不分大小寫的比較int _wcsicmp( const wchar_t *string1, const wchar_t *string2);int _stricmp( const char *string1, const char *string2);
只比較前面n個字int wcsncmp( const wchar_t *string1, const wchar_t *string2, size_t count );int strncmp( const char *string1, const char *string2, size_t count );
複製字串wchar_t *wcscpy( wchar_t *strDestination, const wchar_t *strSrc );char *strcpy( char *strDestination, const char *strSrc );
算字串的長度size_t wcslen( const wchar_t *str);size_t strlen( const char *str);

在上面這個表的右邊是以前介紹過的ASCII字串處理函式,你可以看得出來 Unicode 版本的函式和 ASCII 版本的函式的差異,只有在左邊的 Unicode 版本都是以 wchar_t * 為輸入型別,右邊的 ASCII 本本是以 char * 為輸入型別,還有就是函式名稱如紅字所示,左邊都是 wcs (wide-char string)而右邊都是 str,除此之外使用法完全相同。

既然如此,可不可以這樣做:


#ifdef _UNICODE
    typedef TCHAR whcar_t;
    #define _T(x) L ## x
    #define _tcscpy wcscpy
    #define _tmain wmain
#else
    typedef TCHAR char;
    #define _T(x) x
    #define _tcscpy strcpy
    #define _tmain main
#endif
如此一來只要使用 TCHAR 型別、_T( ) 巨集、以及 tcs 系列的函式,這份程式碼在 unicode 程式當中便會自動地使用 wchar_t 型別及其相關的函式來處理 unicode 字串,而在非 unicode 的程式當中則會自動地以 char 型別及其相關函式來處理 ASCII 字串,所以這份程式碼可以是用於 unicode 專案以及非 unicode 專案,事實上,以上這段程式碼就是 tchar.h 的內容。

除了 #include 以外再多 #include ,你就有 TCHAR 型別、_T( ) 巨集、以及以下的函式可用:

TCHAR 版本
區分大小寫的比較int tcscmp( const TCHAR *string1, const TCHAR *string2);
不分大小寫的比較int _tcsicmp( const TCHAR  *string1, const TCHAR  *string2);
只比較前面n個字int tcsncmp( const TCHAR  *string1, const TCHAR  *string2, size_t count );
複製字串TCHAR  *tcscpy( TCHAR  *strDestination, const TCHAR  *strSrc );
算字串的長度size_t tcslen( const TCHAR  *str);

當你的專案中有 _UNICODE 定義的時候,以上的函式都會變成 wcs 版本,否則會變成 str 版本,例如這樣的原始程式碼:


TCHAR thename[8], myname[8] = _T("SKY");
tcscpy(thename, myname);
_tprintf(_T("%s\n"), thename);

當專案屬性是「使用 Unicode 字元集」的時候,它們會被解讀成:

wchar_t thename[8], myname[8] = L"SKY";
wcscpy(thename, myname);
wprintf(L"%s\n", thename);

否則當專案屬性是「使用多位元組字元集」的時候,它們會被解讀成:

char thename[8], myname[8] = _"SKY";
strcpy(thename, myname);
printf("%s\n", thename);

其中 wprintf( ) 和 _tprintf( ) 分別是 unicode 版本與 TCHAR 版本的字串輸出函式,除此之外,所有的字串處理與輸出入函式也都有 unicode 版本與 TCHAR 版本,包括先前介紹過的 atoi( ) 和 atof( ) 也有 _wtoi( ) 與 _wtof( ) 版本,strtok( ) 也有 wcstok( ) 版本,當然它們也都有 _ttoi( ) 與 _ttof( ) 以及 tcstok( ) 版本的函式。

Unicode 版本檔案函式

除了字串處理以外,文字檔案的處理函式也有 unicode 版本與 TCHAR 版本如下:

Unicode版本TCHAR版本
以 Unicode 字串檔名開啟檔案FILE *_wfopen(const wchar_t *filename, const wchar_t *mode);_tfopen( ) 
從檔案中讀取 Unicode 字串wchar_t *fgetws(wchar_t* string, int n, FILE* stream);fgetts( )
寫 Unicode 字串到檔案中iint fputws(const wchar_t *str, FILE *stream);_fputts( )

當然它們的是使用方法和 ASCII 版本得 fopen( )、fgets( )、以及 fputs( ) 完全相同,其中 _wfopen( ) 以 unicode 字串為檔名來開啟檔案自然沒有問題,但是文字檔案不是 ASCII 的嗎?(或者應該說是各國各自採用的內碼),那麼 fgetws( ) 從 ASCII 內碼的檔案讀入文字,讀入後變成 uncide 字串?而 fputws( ) 寫入 unicode 字串到 ASCII 檔案?

假設有個 a.txt 的內容是"你好" (ASCII字串),fgetws( ) 從 a.txt 能讀取到 L "你好" (Unicode 字串)?而 fputws( ) 寫入 L "你好" (Unicode 字串) 到 a.txt 變成 "你好" (ASCII字串) 嗎?為甚麼會有這種效果?其實就是字碼頁的作用,因為你的電腦採用 950 號字碼頁 (繁體中文):

fgetws( ): Windows 依字碼頁將讀入字串轉為 Unicode
fputws( ): Windows 依字碼頁將寫入字串轉為 ASCII
所以在 unicode 程式當中以這些函式去存取純文字檔案的時候,你不需要去關心這些內碼轉換方面的問題,Windows 已經暗中幫你做掉了,以下的 UniDumpFile 範例以 unicode 版本的函式去 dump 一個文字檔案,其實這是先前的 DumpTextFile 範例的 unicode 版本,除了逐行讀入以外,這個範例還利用空白逐字切割字串,以展示相關函式的使用:

#include <tchar.h>
#include <stdio.h>

void _tmain(void)
{
    TCHAR buffer[256];

    // 嘗試開啟ReadMe.txt 檔案:
    FILE * stream = _tfopen(_T("ReadMe.txt"), _T("rt"));
    TCHAR * token = NULL;

    if (stream)
    {
        // 如果檔案開啟成功,以 fgetws( ) 逐行讀取:
        while(_fgetts(buffer, 256, stream) != NULL)
        {
            token = _tcstok buffer, _T(" \r\n"));
            while (token != NULL)
            {
                _tprintf(_T("%s\n"), token);
                token = _tcstok(NULL, _T(" \r\n"));
            }
        }

        // 關閉檔案:
        fclose(stream);
    }
    else
    {   _tprintf( _T("檔案不存在喔! "));   }
}

這個程式的執行結果如下,你可以看到雖然 ReadMe.txt 是一個採用 Big-5 碼的繁體中文純文字檔案,它仍然可以被此 _fgetts( ) (也就是 fgetws( )) 正確地讀入為 unicode 字串,並且被 _tcstok( ) 正確地切割了。

這是一個
被Dump的
ASCII檔案.
Say
Hello
to
Unicode!

Unicode 程式設計摘要

專案屬性選擇「使用 Unicode 字元集」。
#include
用 _tmain 代替 main。
用 TCHAR 代替 char。
字串前後加上 _T( ),字元前後也加上 _T( )。
使用 tcsXXX 系列的字串函式取代 strXXX。
使用 _tXXX 系列的檔案字串函式,各字串函式的 TCHAR 版為何請查閱 MSDN。
本篇重點回顧

您知道了嗎?
所有的 ASCII 字串函式都有 Unicode 版本(str→wcs)。
#include 以後有 _T( ) 和 tcs 系列函式巨集。
以 Unicode 函式處理 ASCII 文字檔案時,Windows 會依照系統目前的字碼頁(code page)做內碼轉換。

後記:
原站似乎打不太開,先備份這篇好文章,如有問題請告知。