上一篇我們學習了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語言規則,字符串以空字符0x00為結束符,因此使用printf和%s來輸出時,系統並不知道這個h是一個寬字符,而是以此向后一直到空字符,這里剛好第二個就碰上了,因此只能輸出一個“h”。
同樣,scanf函數也是如此:
scanf("%s", str); //這個是C語言的正常用法
scanf("%s", wstr); //這個是可以工作的,但是接收結果是ANSI格式的字符串
scanf("%S", wstr); //這個可以正確接收寬字符格式的字符串
wscanf(L"%s", wstr); //這個是標准的接收寬字符格式字符串
以上的printf和scanf用%S來處理寬字符的方式是微軟擴展的,不一定其他編譯系統也能這樣處理。
- Unicode字符串支持函數
從上面我們看出,微軟的編譯器對寬字符及寬字符串常量用一個大寫的“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版本。
- 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)技術大牛做朋友,在線交流編程經驗,獲取編程基礎知識,解決編程問題。程序員互動聯盟,開發人員自己的家。
轉載請注明出處,謝謝合作!