標籤:
上一篇我們學習了Windows編程的文本及字型輸出,在以上幾篇的執行個體中也出現了一些帶有“TEXT”的Windows宏定義,有朋友留言想瞭解一些ANSI和Unicode編程方面的內容,本章就來瞭解和學習一些Windows下關於ANSI和Unicode方面的編程基礎。
電腦最早在美國誕生,所以最開始都是以英語為作為互動語言,由於只有26個字母,用一個位元組(範圍-128 ~ 127)表示,這個範圍足夠表示26個因為字元和一些常用的控制字元,這個就是ASCII編碼。因此最早的各種程式設計語言以及使用的字串都用位元組數組表示,也確實滿足了編程的各種需求。但是隨著電腦的普及,範圍上逐漸超出了英語使用的國家,這樣一來,字元編碼就成了問題,因為很多國家的語言字元數目根本不能用一個位元組來表示,比如我們國家的中文,常用的就有四千多個,如果再加上一些不常用的字元,更是遠遠不止這些,因此一個位元組的字串編碼就行不通了,那麼自然而然就出現了兩個位元組甚至跟多位元組的編碼方式了。
除了基本的ASCII編碼外,目前常用的字元編碼有MBCS、BG2312、GBK、UTF-8、UTF-16、 UTF-32、BIG5、Base64、Unicode等等,其實Unicode就是使用UTF-16編碼。現在的所有系統都支援多位元組編碼,Windows98以前的對Unicode支援不好,很多核心功能都需要將字串轉換之後才能處理,從Windows NT系統後幾乎都採用了Unicode編碼重新系統核心,非Unicode的編碼會經過轉換之後在傳入核心處理。
在C語言誕生的時候,同樣還沒有遇到多位元組字串問題,當然也沒有Unicode等這些編碼,標準的C語言庫函數處理字串時都是ASCII編碼,因此用標C函數處理多位元組字元編碼就存在問題,所以不同系統都在內部進行這種字元編碼的處理。那麼問題來了,既然標C不支援Unicode,我們又如何編程使用Unicode呢?我們如何指定程式中的字串採用ASCII還是Unicode或者兩種同時出現在一個程式裡面呢? 更好的情況,我們如何編寫程式,根據自己的需求編譯ASCII和Unicode(以下稱寬字元)版本?本文我們就來談談這個問題。在微軟公司提供的C/C++編譯器中提供了一個wchar_t的變數類型,這個類型實際上是通過typedef定義的一個無符號16位整型數。我們使用這個來定義寬字元版本的字元和字串,而普通的ANSI還是標準C語言的char來定義。
下面我們對比一下ASCII和Unicode字元(串)的定義及常量的定義方式。
ASCII版本:
Char c = ‘A’;Char str[] = “hello, world”;
寬字元版本:
wchar_t wch = L’A’;wchar_t wstr[] = L“hello, world”;
微軟的編譯器通過這個大寫字母“L”開頭來識別後面的字串將編譯為一個Unicode的字元或字串,注意這裡的L後面不能有空格。
看下面的執行個體:
#include <windows.h>#include <stdio.h>int main(void){char c = ‘A‘;char str[] = "hello, ANSI";wchar_t wch = L‘A‘;wchar_t wstr[] = L"hello, Unicode";printf("1 --> %c\n", c);printf("2 --> %s\n", str);printf("3 --> %c\n", wch);printf("4 --> %s\n", wstr);printf("5 --> %C\n", c);printf("6 --> %S\n", wstr);wprintf(L"7 --> %c\n", wch);wprintf(L"8 --> %s\n\n", wstr);system("pause");return 0;}
這個小程式的輸出如下:
可以看出:
- 用printf可以輸出ANSI的字元和字串(廢話)
- 用wprintf可以輸出Unicode字元和字串
- printf可以用大寫的字母C、S,即“%C”“%S”來輸出寬字元和字串
- 可以看出第3和第4用printf可以輸出寬字元,但寬字元串僅僅輸出了字串的第一個字元,實際上這個就是問題了,不能這樣輸出,第3的字元A實際上完全是運氣好,因為Unicode是雙位元組,所以寬字元”A”實際在是十六進位的“00 41”,而Windows系統是一個小端系統,所以在記憶體中的排版為“41 00 ……”,所以第一個剛好輸出A。而第4隻能輸出一個“h”,也是因為這個原因。字串wstr在記憶體的存在形式如下如:
第一個字元是“h”,它的寬字元在記憶體排布(小端系統)為”68 00 …”,根據C語言規則,字串以Null 字元0x00為結束符,因此使用printf和%s來輸出時,系統並不知道這個h是一個寬字元,而是以此向後一直到Null 字元,這裡剛好第二個就碰上了,因此只能輸出一個“h”。
同樣,scanf函數也是如此:
scanf("%s", str); //這個是C語言的正常用法
scanf("%s", wstr); //這個是可以工作的,但是接收結果是ANSI格式的字串
scanf("%S", wstr); //這個可以正確接收寬字元格式的字串
wscanf(L"%s", wstr); //這個是標準的接收寬字元格式字串
以上的printf和scanf用%S來處理寬字元的方式是微軟擴充的,不一定其他編譯系統也能這樣處理。
從上面我們看出,微軟的編譯器對寬字元及寬字元串常量用一個大寫的“L”作為首碼來高手編譯,後面的字串作為Unicode版本而不是ANSI版本。另外printf和scanf也有對於的寬字元版本函數wprintf和wscanf來處理,從MSDN我們知道,所有關於字元/字串都有兩個版本,比如_wfopen、_getws、wcslen、wcscpy、wcscat等就是標準C函數fopen、gets、strlen、strcpy、strcat的寬字元版本。除了這些標C的寬字元函數外,Windows的API同樣有ANSI和Unicode版本,比如建立表單和空間的CreateWindowA、CreateProcessA等就是ANSI版本,而對應的CreateWindowW、CreateProcessW就是Unicode版本,他們處理的字串類型都必須是wchar_t的字串。
在一個程式裡面,我們可以使用ANSI版本的函數來處理對應的字串,同時也可以使用Unicode版本的函數來處理wchar_t的字串,正如上面的執行個體一樣,但必須對應,否則可能出現編譯錯誤,更麻煩的是有可能編譯通過但是結果卻不是我們想要的,如上面的第4一條輸出。
當然如果不是需要,最好不要在程式裡面一會兒使用ANSI,一會使用Unicode,這樣對將來的移植性相容性很差,也不利於多語種和國際化。強烈建議使用Unicode版本來編寫程式,這個是一個大趨勢,如果你要把PC平台的Windows程式移植到微軟的嵌入式平台Win CE上的話,就必須是Unicode。微軟為了簡化和通用性,在Win CE平台上只支援Unicode。而且使用Unicode編碼時運行效率更高,因為現在的Windows作業系統核心全部都是用Unicode版本,如果上面傳入一個ANSI的,它必須先轉換成Unicode字串,再傳入內部的函數處理。
當然理想情況是如果編寫統一的應用程式,在編譯時間想編譯成ANSI就編譯成ANSI版本,想編譯成Unicode版本就編譯成Unicode版本是最好的,這樣我們寫出來的程式不管是移植性還是通用性都最好,其實這個微軟早就想到了。
微軟針對標準C函數構造了一套平台相關的字串處理宏定義,所謂平台相關就是說這些宏是微軟自己定義的,只是在Windows平台下使用,不是標準裡面的東西。這些定義在不同的情況下會變成不同的版本。如果定義了“_UNICODE”這個宏定義,Windows將在處理C/C++函數是採用Unicode版本,否則就是ANSI版本。下面我們以strlen這個函數來看一下Windows是怎麼定義的:
#ifdef _UNICODE#define _tcslen wcslen#else#define _tcslen strlen#endif
這裡的_tcslen就是那個平台相關的求字串的字元長度的宏定義,當然我們在使用的時候把他看成函數就行了,可以看到如果定義了_UNICODE,那麼_tcslen在編譯時間實際是連結的wcslen,否則連結strlen。現在我們開啟VS下面的標頭檔“tchar.h”,就可以看到很多以底線開頭的宏定義,這些都是平台相關的通用字串處理庫函數:
所以使用這些函數的時候要包含這個標頭檔。
另外,如果定義了“UNCODE”這個宏,Windows的API也會採用Unicode版本,否則採用ANSI版本。比如CreateWindow這個函數定義如下:
#ifdef UNICODE#define CreateWindow CreateWindowW#else#define CreateWindow CreateWindowA#endif // !UNICODE
所以實際上CreateWindow是一個宏定義而已,但是這不影響我們把它當做函數來使用,同樣其他含有字串作為參數的Windows API也同樣做了定義。
預設情況下,我們使用VS來建立工程,_UNICODE和UNICODE這兩個宏都是開啟的,所以我們用嚮導建立的工程都是Unicode版本的,我們也可以從配置選項裡面刪除這兩個定義來編譯ANSI版本的程式。
現在函數的使用解決了,那麼如何來定義字元以及字串的變數類型已經常量,使得_UNICODE和UNICODE定義也能影響類型和常量呢?微軟同樣使用了一系列的定義來解決這個問題。TCHAR是作為字元、字串的變數類型,等價於char和wchar_t,如果定義了UNICDOE,TCHAR實際上是wchar_t,否則就是char,這個在winnt.h中能找到。
對字串常量,VS定義了TEXT、__TEXT,在tchar.h中,還定義了_T等好幾種方式,只要定義了UNICODE,則這些宏定義就是Unicode,否則就是ANSI版本。因此我們以後在編寫程式時,應該充分用這些宏來定義字串類型變數,常量以及處理函數。下面是一個推薦的簡單一實例:
#include <windows.h>#include <tchar.h>int _tmain(void){TCHAR c = TEXT(‘A‘);TCHAR buf[16];TCHAR *str = TEXT("hello, world!");_tprintf(TEXT("1 --> %c\n"), c);_tprintf(TEXT("2 --> %s\n"), str);_tscanf(_T("%s"), buf);_tprintf(_T("%s\n"), buf);_tsystem(TEXT("pause"));return 0;}
在這個執行個體中,所有可能用到字串的函數都採用通用的函數,能正確的編譯Unicode版本和ANSI版本。
有時候我們可能還是會出現不同編碼之間的轉換,這是我們可以採用Windows提供的API來完成。
MultiByteToWideChar函數和WideCharToMultiByte函數,這兩個函數可以在ANSI和Unicode字串之間來迴轉換。他們的參數有很多相似之處,原型為:
int MultiByteToWideChar(UINT CodePage, DWORD dwFlags, LPCSTR lpMultiByteStr, int cbMultiByte, LPWSTR lpWideCharStr, int cchWideChar);int WideCharToMultiByte(UINT CodePage, DWORD dwFlags, LPCWSTR lpWideCharStr, int cchWideChar, LPSTR lpMultiByteStr, int cbMultiByte, LPCSTR lpDefaultChar, LPBOOL lpUsedDefaultChar);
具體用法可以參考MSDN,網上也能找到大量的使用說明和執行個體,這裡就不再敘述。
下面給一個執行個體來示範ANSI和Unicode之間的轉換:
#include <windows.h>#include <tchar.h>#include <stdio.h>int _tmain(void){int nwCh;char AnsiStr[] = "hello, world!";wchar_t wszBuf[20] = {0};//獲得轉換後產生多少Unicode字元,可以作為後面實際轉換時傳入容納轉換結果的Unicode字元數buffer大小nwCh = MultiByteToWideChar(CP_ACP, 0, AnsiStr, -1, NULL, 0);//轉換並接收結果MultiByteToWideChar(CP_ACP, 0, AnsiStr, -1, wszBuf, nwCh);wprintf(L"nwCh = %d, %s\n", nwCh, wszBuf);int nCh;char AnsiBuf[20] = {0};//獲得轉換後產生多少ANSI字元,可以作為後面實際轉換時傳入容納轉換結果的ANSI字元數buffer大小nCh = WideCharToMultiByte(CP_ACP, 0, wszBuf, -1, NULL, 0, NULL, NULL);//轉換並接收結果WideCharToMultiByte(CP_ACP, 0, wszBuf, -1, AnsiBuf, nCh, NULL, NULL);printf("nCh = %d, %s\n", nCh, AnsiBuf);_tsystem(TEXT("pause"));return 0;}
請注意注釋部分,該函數及可以轉換,也能擷取轉後所需輸出的儲存字元個數空間的大小。運行後的輸出結果:
到這裡本文就結束了,下一篇將繼續我們的Windows編程系列之旅。敬請關注!
關注公眾平台:程式員互動聯盟(coder_online),你可以第一時間擷取原創技術文章,和(java/C/C++/Android/Windows/Linux)技術大牛做朋友,線上交流編程經驗,擷取編程基礎知識,解決編程問題。程式員互動聯盟,開發人員自己的家。
轉載請註明出處,謝謝合作!
【Windows編程】系列第四篇:使用Unicode編程