Unicode簡介和使用

一、Unicode簡介

在第一章中,我已經預告,C語言中在Microsoft Windows程序設計中扮演著重要角色的任何部分都會講述到,您也許在傳統文字模式程序設計中還尚未遇到過這些問題。寬字符集和Unicode差不多就是這樣的問題。

簡單地說,Unicode擴展自ASCII字符集。在嚴格的ASCII中,每個字符用7位表示,或者計算機上普遍使用的每字符有8位寬;而Unicode使用全16位字符集。這使得Unicode能夠表示世界上所有的書寫語言中可能用于計算機通訊的字符、象形文字和其它符號。Unicode最初打算作為ASCII的補充,可能的話,最終將代替它。考慮到ASCII是計算機中最具支配地位的標準,所以這的確是一個很高的目標。

Unicode影響到了計算機工業的每個部分,但也許會對操作系統和程序設計語言的影響最大。從這方面來看,我們已經上路了。Windows NT從底層支持Unicode(不幸的是,Windows 98只是小部分支持Unicode)。先天即被ANSI束縛的C程序設計語言通過對寬字符集的支持來支持Unicode。下面將詳細討論這些內容。

自然,作為程序寫作者,我們通常會面對許多繁重的工作。我已試圖透過使本書中的所有程序「Unicode化」來減輕負擔。其含義會隨著本章對Unicode的討論而清晰起來。

雖然不能確定人類開始講話的時間,但書寫已有大約6000年的歷史了。實際上,早期書寫的內容是象形文字。每個字符都對應于發聲的字母表則出現于大約3000年前。雖然人們過去使用的多種書寫語言都用得好好的,但19世紀的幾個發明者還是看到了更多的需求。Samuel F. B. Morse在1838年到1854年間發明了電報,當時他還發明了一種電報上使用的代碼。字母表中的每個字符對應于一系列短的和長的脈沖(點和破折號)。雖然其中大小寫字母之間沒有區別,但數字和標點符號都有了自己的代碼。

Morse代碼并不是以其它圖畫的或印刷的象形文字來代表書寫語言的第一個例子。1821年到1824年之間,年輕的Louis Braille受到在夜間讀寫信息的軍用系統的啟發,發明了一種代碼,它用紙上突起的點作為代碼來幫助盲人閱讀。Braille代碼實際上是一種6位代碼,它把字符、常用字母組合、常用單字和標點進行編碼。一個特殊的escape代碼表示后續的字符代碼應解釋為大寫。一個特殊的shift代碼允許后續代碼被解釋為數字。

Telex代碼,包括Baudot (以一個法國工程師命名,該工程師死于1903年)以及一種被稱為CCITT #2的代碼(1931年被標準化),都是包括字符和數字的5位代碼。


早期計算機的字符碼是從Hollerith卡片(號稱不能被折迭、卷曲或毀傷)發展而來的,該卡片由Herman Hollerith發明并首次在1890年的美國人口普查中使用。6位字符碼系統BCDIC(Binary-Coded Decimal Interchange Code:二進制編碼十進制交換編碼)源自Hollerith代碼,在60年代逐步擴展為8位EBCDIC,并一直是IBM大型主機的標準,但沒使用在其它地方。

美國信息交換標準碼(ASCII:American Standard Code for Information Interchange)起始于50年代后期,最后完成于1967年。開發ASCII的過程中,在字符長度是6位、7位還是8位的問題上產生了很大的爭議。從可靠性的觀點來看不應使用替換字符,因此ASCII不能是6位編碼,但由于費用的原因也排除了8位版本的方案(當時每位的儲存空間成本仍很昂貴)。這樣,最終的字符碼就有26個小寫字母、26個大寫字母、10個數字、32個符號、33個句柄和一個空格,總共128個字符碼。ASCII現在記錄在ANSI X3.4-1986字符集-用于信息交換的7位美國國家標準碼(7-Bit ASCII:7-Bit American National Standard Code for Information Interchange),由美國國家標準協會(American National Standards Institute)發布。圖2-1中所示的ASCII字符碼與ANSI文件中的格式相似。

ASCII有許多優點。例如,26個字母代碼是連續的(在EBCDIC代碼中就不是這樣的);大寫字母和小寫字母可通過改變一位數據而相互轉化;10個數字的代碼可從數值本身方便地得到(在BCDIC代碼中,字符「0」的編碼在字符「9」的后面!)

最棒的是,ASCII是一個非常可靠的標準。在鍵盤、視訊顯示卡、系統硬件、打印機、字體文件、操作系統和Internet上,其它標準都不如ASCII碼流行而且根深蒂固。

ASCII的最大問題就是該縮寫的第一個字母。ASCII是一個真正的美國標準,所以它不能良好滿足其它講英語國家的需要。例如英國的英鎊符號(£)在哪里?

英語使用拉丁(或羅馬)字母表。在使用拉丁語字母表的書寫語言中,英語中的單詞通常很少需要重音符號(或讀音符號)。即使那些傳統慣例加上讀音符號也無不當的英語單字,例如c鰋perate或者résumé,拼寫中沒有讀音符號也會被完全接受。

但在美國以南、以北,以及大西洋地區的許多國家,在語言中使用讀音符號很普遍。這些重音符號最初是為使拉丁字母表適合這些語言讀音不同的需要。在遠東或西歐的南部旅游,您會遇到根本不使用拉丁字母的語言,例如希臘語、希伯來語、阿拉伯語和俄語(使用斯拉夫字母表)。如果您向東走得更遠,就會發現中國象形漢字,日本和朝鮮也采用漢字系統。

ASCII的歷史開始于1967年,此后它主要致力于克服其自身限制以更適合于非美國英語的其它語言。例如,1967年,國際標準化組織(ISO:International Standards Organization)推薦一個ASCII的變種,代碼0x40、0x5B、0x5C、0x5D、0x7B、0x7C和0x7D「為國家使用保留」,而代碼0x5E、0x60和0x7E標為「當國內要求的特殊字符需要8、9或10個空間位置時,可用于其它圖形符號」。這顯然不是一個最佳的國際解決方案,因為這并不能保證一致性。但這卻顯示了人們如何想盡辦法為不同的語言來編碼的。

在小型計算機開發的初期,就已經嚴格地建立了8位字節。因此,如果使用一個字節來保存字符,則需要128個附加的字符來補充ASCII。1981年,當最初的IBM PC推出時,視訊卡的ROM中燒有一個提供256個字符的字符集,這也成為IBM標準的一個重要組成部分。

最初的IBM擴展字符集包括某些帶重音的字符和一個小寫希臘字母表(在數學符號中非常有用),還包括一些塊型和線狀圖形字符。附加的字符也被添加到ASCII控制字符的編碼位置,這是因為大多數控制字符都不是拿來顯示用的。

該IBM擴展字符集被燒進無數顯示卡和打印機的ROM中,并被許多應用程序用于修飾其文字模式的顯示方式。不過,該字符集并沒有為所有使用拉丁字母表的西歐語言提供足夠多的帶重音字符,而且也不適用于Windows。Windows不需要圖形字符,因為它有一個完全圖形化的系統。

在Windows 1.0(1985年11月發行)中,Microsoft沒有完全放棄IBM擴展字符集,但它已退居第二重要位置。因為遵循了ANSI草案和ISO標準,純Windows字符集被稱作「ANSI字符集」。ANSI草案和ISO標準最終成為ANSI/ISO 8859-1-1987,即「American National Standard for Information Processing-8-Bit Single-Byte Coded Graphic Character Sets-Part 1: Latin Alphabet No 1」,通常也簡寫為「Latin 1」。

在Windows 1.0的《Programmer's Reference》中印出了ANSI字符集的最初版本,如圖2-2所示。


空方框表示該位置未定義字符。這與ANSI/ISO 8859-1的最終定義一致。ANSI/ISO 8859-1僅顯示了圖形字符,而沒有控制字符,因此沒有定義DEL。此外,代碼0xA0定義為一個非斷開的空格(這意味著在編排格式時,該字符不用于斷開一行),代碼0xAD是一個軟連字符(表示除非在行尾斷開單詞時使用,否則不顯示)。此外,ANSI/ISO 8859-1將代碼0xD7定義為乘號(*),0xF7為除號(/)。Windows中的某些字體也定義了從0x80到0x9F的某些字符,但這些不是ANSI/ISO 8859-1標準的一部分。

MS-DOS 3.3(1987年4月發行)向IBM PC用戶引進了代碼頁(code page)的概念,Windows也使用此概念。代碼頁定義了字符的映像代碼。最初的IBM字符集被稱作代碼頁437,或者「MS-DOS Latin US)。代碼頁850就是「MS-DOS Latin 1」,它用附加的帶重音字母(但不是圖2-2所示的Latin 1 ISO/ANSI標準)代替了一些線形字符。其它代碼頁被其它語言定義。最低的128個代碼總是相同的;較高的128個代碼取決于定義代碼頁的語言。

在MS-DOS中,如果用戶為PC的鍵盤、顯示卡和打印機指定了一個代碼頁,然后在PC上創建、編輯和打印文件,一切都很正常,每件事都會保持一致。然而,如果用戶試圖與使用不同代碼頁的用戶交換文件,或者在機器上改變代碼頁,就會產生問題。字符碼與錯誤的字符相關聯。應用程序能夠將代碼頁信息與文件一起保存來試圖減少問題的產生,但該策略包括了某些在代碼頁間轉換的工作。

雖然代碼頁最初僅提供了不包括帶重音符號字母的附加拉丁字符集,但最終代碼頁的較高的128個字符還是包括了完整的非拉丁字母,例如希伯來語、希臘語和斯拉夫語。自然,如此多樣會導致代碼頁變得混亂;如果少數帶重音的字母未正確顯示,那么整個文字便會混亂不堪而不可閱讀。

代碼頁的擴展正是基于所有這些原因,但是還不夠。斯拉夫語的MS-DOS代碼頁855與斯拉夫語的Windows代碼頁1251以及斯拉夫語的Macintosh代碼頁10007不同。每個環境下的代碼頁都是對該環境所作的標準字符集修正。IBM OS/2也支援多種EBCDIC代碼頁。

但等一下,你會發現事情變得更糟糕。

迄今為止,我們已經看到了256個字符的字符集。但中國、日本和韓國的象形文字符號有大約21,000個。如何容納這些語言而仍保持和ASCII的某種兼容性呢?

解決方案(如果這個說法正確的話)是雙字節字符集(DBCS:double-byte character set)。DBCS從256代碼開始,就像ASCII一樣。與任何行為良好的代碼頁一樣,最初的128個代碼是ASCII。然而,較高的128個代碼中的某些總是跟隨著第二個字節。這兩個字節一起(稱作首字節和跟隨字節)定義一個字符,通常是一個復雜的象形文字。

雖然中文、日文和韓文共享一些相同的象形文字,但顯然這三種語言是不同的,而且經常是同一個象形文字在三種不同的語言中代表三件不同的事。Windows支持四個不同的雙字節字符集:代碼頁932(日文)、936(簡體中文)、949(韓語)和950(繁體漢字)。只有為這些國家(地區)生產的Windows版本才支持DBCS。

雙字符集問題并不是說字符由兩個字節代表。問題在于一些字符(特別是ASCII字符)由1個字節表示。這會引起附加的程序設計問題。例如,字符串中的字符數不能由字符串的字節數決定。必須剖析字符串來決定其長度,而且必須檢查每個字節以確定它是否為雙字節字符的首字節。如果有一個指向DBCS字符串中間的指針,那么該字符串前一個字符的地址是什么呢?慣用的解決方案是從開始的指針分析該字符串!

我們面臨的基本問題是世界上的書寫語言不能簡單地用256個8位代碼表示。以前的解決方案包括代碼頁和DBCS已被證明是不能滿足需要的,而且也是笨拙的。那什么才是真正的解決方案呢?

身為程序寫作者,我們經歷過這類問題。如果事情太多,用8位數值已經不能表示,那么我們就試更寬的值,例如16位值。而且這很有趣的,正是Unicode被制定的原因。與混亂的256個字符代碼映像,以及含有一些1字節代碼和一些2字節代碼的雙字節字符集不同,Unicode是統一的16位系統,這樣就允許表示65,536個字符。這對表示所有字符及世界上使用象形文字的語言,包括一系列的數學、符號和貨幣單位符號的集合來說是充裕的。

明白Unicode和DBCS之間的區別很重要。Unicode使用(特別在C程序設計語言環境里)「寬字符集」。「Unicode中的每個字符都是16位寬而不是8位寬。」在Unicode中,沒有單單使用8位數值的意義存在。相比之下,在雙字節字符集中我們仍然處理8位數值。有些字節自身定義字符,而某些字節則顯示需要和另一個字節共同定義一個字符。

處理DBCS字符串非常雜亂,但是處理Unicode文字則像處理有秩序的文字。您也許會高興地知道前128個Unicode字符(16位代碼從0x0000到0x007F)就是ASCII字符,而接下來的128個Unicode字符(代碼從0x0080到0x00FF)是ISO 8859-1對ASCII的擴展。Unicode中不同部分的字符都同樣基于現有的標準。這是為了便于轉換。希臘字母表使用從0x0370到0x03FF的代碼,斯拉夫語使用從0x0400到0x04FF的代碼,美國使用從0x0530到0x058F的代碼,希伯來語使用從0x0590到0x05FF的代碼。中國、日本和韓國的象形文字(總稱為CJK)占用了從0x3000到0x9FFF的代碼。

Unicode的最大好處是這里只有一個字符集,沒有一點含糊。Unicode實際上是個人計算機行業中幾乎每個重要公司共同合作的結果,并且它與ISO 10646-1標準中的代碼是一一對應的。Unicode的重要參考文獻是《The Unicode Standard,Version 2.0》(Addison-Wesley出版社,1996年)。這是一本特別的書,它以其它文件少有的方式顯示了世界上書寫語言的豐富性和多樣性。此外,該書還提供了開發Unicode的基本原理和細節。

Unicode有缺點嗎?當然有。Unicode字符串占用的內存是ASCII字符串的兩倍。(然而壓縮文件有助于極大地減少文件所占的磁盤空間。)但也許最糟的缺點是:人們相對來說還不習慣使用Unicode。身為程序寫作者,這就是我們的工作。

對C程序寫作者來說,16位字符的想法的確讓人掃興。一個char和一個字節同寬是最不能確定的事情之一。沒幾個程序寫作者清楚ANSI/ISO 9899-1990,這是「美國國家標準程序設計語言-C」(也稱作「ANSI C」)通過一個稱作「寬字符」的概念來支持用多個字節代表一字符的字符集。這些寬字符與常用的字符完美地共存。

ANSI C也支持多字節字符集,例如中文、日文和韓文版本Windows支持的字符集。然而,這些多字節字符集被當成單字節構成的字符串看待,只不過其中一些字符改變了后續字符的含義而已。多字節字符集主要影響C語言程序執行時期鏈接庫函數。相比之下,寬字符比正常字符寬,而且會引起一些編譯問題。

寬字符不需要是Unicode。Unicode是一種可能的寬字符集。然而,因為本書的焦點是Windows而不是C執行的理論,所以我將把寬字符和Unicode作為同義語。

假定我們都非常熟悉在C程序中使用char數據型態來定義和儲存字符跟字符串。但為了便于理解C如何處理寬字符,讓我們先回顧一下可能在Win32程序中出現的標準字符定義。

下面的語句定義并初始化了一個只包含一個字符的變量:

char c = 'A' ;

變量c需要1個字節來保存,并將用十六進制數0x41初始化,這是字母A的ASCII代碼。

您可以像這樣定義一個指向字符串的指針:

char * p ;

因為Windows是一個32位操作系統,所以指針變量p需要用4個字節保存。您還可初始化一個指向字符串的指針:

char * p = "Hello!" ;

像前面一樣,變量p也需要用4個字節保存。該字符串保存在靜態內存中并占用7個字節-6個字節保存字符串,另1個字節保存終止符號0。

您還可以像這樣定義字符數組:

char a[10] ;

在這種情況下,編譯器為該數組保留了10個字節的儲存空間。表達式sizeof(a)將返回10。如果數組是整體變量(即在所有函數外定義),您可使用像下面的語句來初始化一個字符數組:

char a[] = "Hello!" ;

如果您將該數組定義為一個函數的區域變量,則必須將它定義為一個static變量,如下:

static char a[] = "Hello!" ;

無論哪種情況,字符串都儲存在靜態程序內存中,并在末尾添加0,這樣就需要7個字節的儲存空間。

Unicode或者寬字符都沒有改變char數據型態在C中的含義。char繼續表示1個字節的儲存空間,sizeof (char)繼續返回1。理論上,C中1個字節可比8位長,但對我們大多數人來說,1個字節(也就是1個char)是8位寬。

C中的寬字符基于wchar_t數據型態,它在幾個表頭文件包括WCHAR.H中都有定義,像這樣:

typedef unsigned short wchar_t ;

因此,wchar_t數據型態與無符號短整數型態相同,都是16位寬。

要定義包含一個寬字符的變量,可使用下面的語句:

wchar_t c = 'A' ;

變量c是一個雙字節值0x0041,是Unicode表示的字母A。(然而,因為Intel微處理器從最小的字節開始儲存多字節數值,該字節實際上是以0x41、0x00的順序保存在內存中。如果檢查Unicode文字的計算機儲存應注意這一點。)

您還可定義指向寬字符串的指針:

wchar_t * p = L"Hello!" ;

注意緊接在第一個引號前面的大寫字母L(代表「long」)。這將告訴編譯器該字符串按寬字符保存-即每個字符占用2個字節。通常,指針變量p要占用4個字節,而字符串變量需要14個字節-每個字符需要2個字節,末尾的0還需要2個字節。

同樣,您還可以用下面的語句定義寬字符數組:

static wchar_t a[] = L"Hello!" ;

該字符串也需要14個字節的儲存空間,sizeof (a) 將返回14。索引數組a可得到單獨的字符。a[1] 的值是寬字符「e」,或者0x0065。

雖然看上去更像一個印刷符號,但第一個引號前面的L非常重要,并且在兩個符號之間必須沒有空格。只有帶有L,編譯器才知道您需要將字符串存為每個字符2字節。稍后,當我們看到使用寬字符串而不是變量定義時,您還會遇到第一個引號前面的L。幸運的是,如果忘記了包含L,C編譯器通常會給提出警告或錯誤信息。

您還可在單個字符文字前面使用L前綴,來表示它們應解釋為寬字符。如下所示:

wchar_t c = L'A' ;

但通常這是不必要的,C編譯器會對該字符進行擴充,使它成為寬字符。

我們都知道如何獲得字符串的長度。例如,如果我們已經像下面這樣定義了一個字符串指針:

char * pc = "Hello!" ;

我們可以呼叫

iLength = strlen (pc) ;

這時變量iLength將等于6,也就是字符串中的字符數。

太好了!現在讓我們試著定義一個指向寬字符的指針:

wchar_t * pw = L"Hello!" ;

再次呼叫strlen :

iLength = strlen (pw) ;

現在麻煩來了。首先,C編譯器會顯示一條警告消息,可能是這樣的內容:

'function' : incompatible types - from 'unsigned short *' to 'const char *'

這條消息的意思是:聲明strlen函數時,該函數應接收char類型的指標,但它現在卻接收了一個unsigned short類型的指標。您仍然可編譯并執行該程序,但您會發現iLength等于1。為什么?

字符串「Hello!」中的6個字符占用16位:

0x0048 0x0065 0x006C 0x006C 0x006F 0x0021

Intel處理器在內存中將其存為:

48 00 65 00 6C 00 6C 00 6F 00 21 00

假定strlen函數正試圖得到一個字符串的長度,并把第1個字節作為字符開始計數,但接著假定如果下一個字節是0,則表示字符串結束。

這個小練習清楚地說明了C語言本身和執行時期鏈接庫函數之間的區別。編譯器將字符串L"Hello!" 解釋為一組16位短整數型態數據,并將其保存在wchar_t數組中。編譯器還處理數組索引和sizeof操作符,因此這些都能正常工作,但在連結時才添加執行時期鏈接庫函數,例如strlen。這些函數認為字符串由單字節字符組成。遇到寬字符串時,函數就不像我們所希望那樣執行了。

您可能要說:「噢,太麻煩了!」現在每個C語言鏈接庫函數都必須重寫以接受寬字符。但事實上并不是每個C語言鏈接庫函數都需要重寫,只是那些有字符串參數的函數才需要重寫,而且也不用由您來完成。它們已經重寫完了。

strlen函數的寬字符版是wcslen(wide-character string length:寬字符串長度),并且在STRING.H(其中也說明了strlen)和WCHAR.H中均有說明。strlen函數說明如下:

size_t __cdecl strlen (const char *) ;        

而wcslen函數則說明如下:

size_t __cdecl wcslen (const wchar_t *) ;        

這時我們知道,要得到寬字符串的長度可以呼叫

iLength = wcslen (pw) ; 

函數將返回字符串中的字符數6。請記住,改成寬字節后,字符串的字符長度不改變,只是位組長度改變了。

您熟悉的所有帶有字符串參數的C執行時期鏈接庫函數都有寬字符版。例如,wprintf是printf的寬字符版。這些函數在WCHAR.H和含有標準函數說明的表頭文件中說明。

當然,使用Unicode也有缺點。第一點也是最主要的一點是,程序中的每個字符串都將占用兩倍的儲存空間。此外,您將發現寬字符執行時期鏈接庫中的函數比常規的函數大。出于這個原因,您也許想建立兩個版本的程序-一個處理ASCII字符串,另一個處理Unicode字符串。最好的解決辦法是維護既能按ASCII編譯又能按Unicode編譯的單一原始碼文件。

雖然只是一小段程序,但由于執行時期鏈接庫函數有不同的名稱,您也要定義不同的字符,這將在處理前面有L的字符串文字時遇到麻煩。

一個辦法是使用Microsoft Visual C++包含的TCHAR.H表頭文件。該表頭文件不是ANSI C標準的一部分,因此那里定義的每個函數和宏定義的前面都有一條底線。TCHAR.H為需要字符串參數的標準執行時期鏈接庫函數提供了一系列的替代名稱(例如,_tprintf和_tcslen)。有時這些名稱也稱為「通用」函數名稱,因為它們既可以指向函數的Unicode版也可以指向非Unicode版。

如果定義了名為_UNICODE的標識符,并且程序中包含了TCHAR.H表頭文件,那么_tcslen就定義為wcslen:

#define _tcslen wcslen        

如果沒有定義UNICODE,則_tcslen定義為strlen:

#define _tcslen strlen        

等等。TCHAR.H還用一個新的數據型態TCHAR來解決兩種字符數據型態的問題。如果定義了_UNICODE標識符,那么TCHAR就是wchar_t:

typedef wchar_t TCHAR ;        

否則,TCHAR就是Char:

typedef char TCHAR ;        

現在開始討論字符串文字中的L問題。如果定義了_UNICODE標識符,那么一個稱作__T的宏就定義如下:

#define __T(x) L##x        

這是相當晦澀的語法,但合乎ANSI C標準的前置處理器規范。那一對井字號稱為「粘貼符號(token paste)」,它將字母L添加到宏參數上。因此,如果宏參數是"Hello!",則L##x就是L"Hello!"。

如果沒有定義_UNICODE標識符,則__T宏只簡單地定義如下:

#define __T(x) x        

此外,還有兩個宏與__T定義相同:

#define _T(x)__T(x)        
#define _TEXT(x)__T(x)        

在Win32 console程序中使用哪個宏,取決于您喜歡簡潔還是詳細。基本地,必須按下述方法在_T或_TEXT宏內定義字符串文字:

_TEXT ("Hello!")        

這樣做的話,如果定義了_UNICODE,那么該串將解釋為寬字符的組合,否則解釋為8位的字符字符串。

Windows NT從底層支援Unicode。這意味著Windows NT內部使用由16位字符組成的字符串。因為世界上其它許多地方還不使用16位字符串,所以Windows NT必須經常將字符串在操作系統內轉換。Windows NT可執行為ASCII、Unicode或者ASCII和Unicode混合編寫的程序。即,Windows NT支持不同的API函數呼叫,這些函數接受8位或16位的字符串(我們將馬上看到這是如何動作的。)

相對于Windows NT,Windows 98對Unicode的支持要少得多。只有很少的Windows 98函數呼叫支持寬字符串(這些函數列在《Microsoft Knowledge Base article Q125671》中;它們包括MessageBox)。如果要發行的程序中只有一個.EXE文件要求在Windows NT和Windows 98下都能執行,那么就不應該使用Unicode,否則就不能在Windows 98下執行;尤其程序不能呼叫Unicode版的Windows函數。這樣,將來發行Unicode版的程序時會處于更有利的位置,您應試著編寫既為ASCII又為Unicode編譯的原始碼。這就是本書中所有程序的編寫方式。

正如您在第一章所看到的那樣,一個Windows程序包括表頭文件WINDOWS.H。該文件包括許多其它表頭文件,包括WINDEF.H,該文件中有許多在Windows中使用的基本型態定義,而且它本身也包括WINNT.H。WINNT.H處理基本的Unicode支持。

WINNT.H的前面包含C的表頭文件CTYPE.H,這是C的眾多表頭文件之一,包括wchar_t的定義。WINNT.H定義了新的數據型態,稱作CHAR和WCHAR:

typedef char CHAR ;        
typedef wchar_t WCHAR ;    // wc        

當您需要定義8位字符或者16位字符時,推薦您在Windows程序中使用的數據型態是CHAR和WCHAR。WCHAR定義后面的注釋是匈牙利標記法的建議:一個基于WCHAR數據型態的變量可在前面附加上字母wc以說明一個寬字符。

WINNT.H表頭文件進而定義了可用做8位字符串指針的六種數據型態和四個可用做const 8位字符串指針的數據型態。這里精選了表頭文件中一些實用的說明數據型態語句:

typedef CHAR * PCHAR, * LPCH, * PCH, * NPSTR, * LPSTR, * PSTR ;        
typedef CONST CHAR * LPCCH, * PCCH, * LPCSTR, * PCSTR ;        

前綴N和L表示「near」和「long」,指的是16位Windows中兩種大小不同的指標。在Win32中near和long指標沒有區別。

類似地,WINNT.H定義了六種可作為16位字符串指針的數據型態和四種可作為const 16位字符串指針的數據型態:

typedef WCHAR * PWCHAR, * LPWCH, * PWCH, * NWPSTR, * LPWSTR, * PWSTR ;        
typedef CONST WCHAR * LPCWCH, * PCWCH, * LPCWSTR, * PCWSTR ;        

至此,我們有了數據型態CHAR(一個8位的char)和WCHAR(一個16位的wchar_t),以及指向CHAR和WCHAR的指標。與TCHAR.H一樣,WINNT.H將TCHAR定義為一般的字符類型。如果定義了標識符UNICODE(沒有底線),則TCHAR和指向TCHAR的指標就分別定義為WCHAR和指向WCHAR的指標;如果沒有定義標識符UNICODE,則TCHAR和指向TCHAR的指標就分別定義為char和指向char的指標:

#ifdef  UNICODE 
typedef WCHAR TCHAR, * PTCHAR ;
typedef LPWSTR LPTCH, PTCH, PTSTR, LPTSTR ; 
typedef LPCWSTR LPCTSTR ;      
#else
typedef char TCHAR, * PTCHAR ;  
typedef LPSTR LPTCH, PTCH, PTSTR, LPTSTR ; 
typedef LPCSTR LPCTSTR ;   
#endif

如果已經在某個表頭文件或者其它表頭文件中定義了TCHAR數據型態,那么WINNT.H和WCHAR.H表頭文件都能防止其重復定義。不過,無論何時在程序中使用其它表頭文件時,都應在所有其它表頭文件之前包含WINDOWS.H。

WINNT.H表頭文件還定義了一個宏,該宏將L添加到字符串的第一個引號前。如果定義了UNICODE標識符,則一個稱作 __TEXT的宏定義如下:

#define __TEXT(quote) L##quote        

如果沒有定義標識符UNICODE,則像這樣定義__TEXT宏:

#define __TEXT(quote) quote        

此外, TEXT宏可這樣定義:

#define TEXT(quote) __TEXT(quote)        

這與TCHAR.H中定義_TEXT宏的方法一樣,只是不必操心底線。我將在本書中使用這個宏的TEXT版本。

這些定義可使您在同一程序中混合使用ASCII和Unicode字符串,或者編寫一個可被ASCII或Unicode編譯的程序。如果您希望明確定義8位字符變量和字符串,請使用CHAR、PCHAR(或者其它),以及帶引號的字符串。為明確地使用16位字符變量和字符串,請使用WCHAR、PWCHAR,并將L添加到引號前面。對于是8位還是16位取決于UNICODE標識符的定義的變量或字符串,要使用TCHAR、PTCHAR和TEXT宏。

從Windows 1.0到Windows 3.1的16位Windows中,MessageBox函數位于動態鏈接庫USER.EXE。在Windows 3.1軟件開發套件的WINDOWS.H中,MessageBox函數定義如下:

int WINAPI MessageBox (HWND, LPCSTR, LPCSTR, UINT) ;        

注意,函數的第二個、第三個參數是指向常數字符串的指針。當編譯連結一個Win16程序時,Windows并不處理MessageBox呼叫。程序.EXE文件中的表格,允許Windows將該程序的呼叫與USER中的MessageBox函數動態鏈接起來。

32位的Windows(即所有版本的Windows NT,以及Windows 95和Windows 98)除了含有與16位兼容的USER.EXE以外,還含有一個稱為USER32.DLL的動態鏈接庫,該動態鏈接庫含有32位使用者接口函數的進入點,包括32位的MessageBox。

這就是Windows支持Unicode的關鍵:在USER32.DLL中,沒有32位MessageBox函數的進入點。實際上,有兩個進入點,一個名為MessageBoxA(ASCII版),另一個名為MessageBoxW(寬字符版)。用字符串作參數的每個Win32函數都在操作系統中有兩個進入點!幸運的是,您通常不必關心這個問題,程序中只需使用MessageBox。與TCHAR表頭文件一樣,每個Windows表頭文件都有我們需要的技巧。

下面是MessageBoxA在WINUSER.H中定義的方法。這與MessageBox早期的定義很相似:

WINUSERAPI int WINAPI MessageBoxA (HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) ;

下面是MessageBoxW:

WINUSERAPI int WINAPI MessageBoxW (HWND hWnd, LPCWSTR lpText,LPCWSTR lpCaption, UINT uType) ;

注意,MessageBoxW函數的第二個和第三個參數是指向寬字符的指針。

如果需要同時使用并分別匹配ASCII和寬字符函數呼叫,那么您可在Windows程序中明確地使用MessageBoxA和MessageBoxW函數。但大多數程序寫作者將繼續使用MessageBox。根據是否定義了UNICODE,MessageBox將與MessageBoxA或MessageBoxW一樣。在WINUSER.H中完成這一技巧時,程序相當瑣碎:

#ifdef UNICODE#define MessageBox  MessageBoxW#else#define MessageBox  MessageBoxA#endif

這樣,如果定義了UNICODE標識符,那么程序中所有的MessageBox函數呼叫實際上就是MessageBoxW函數;否則,就是MessageBoxA函數。

執行該程序時,Windows將程序中不同的函數呼叫與不同的Windows動態鏈接庫的進入點連結。雖然只有少數例外,但是,在Windows 98中不能執行Unicode版的Windows函數。雖然這些函數有進入點,但通常返回錯誤代碼。應用程序注意這些返回的錯誤并采取一些合理的動作。

正如前面談到的,Microsoft C包括寬字符和需要字符串參數的C語言執行時期鏈接庫函數的所有普通版本。不過,Windows復制了其中一部分。例如,下面是Windows定義的一組字符串函數,這些函數用來計算字符串長度、復制字符串、連接字符串和比較字符串:

ILength = lstrlen (pString) ;pString = lstrcpy (pString1, pString2) ;pString = lstrcpyn (pString1, pString2, iCount) ;pString = lstrcat (pString1, pString2) ;iComp = lstrcmp (pString1, pString2) ;iComp = lstrcmpi (pString1, pString2) ;

這些函數與C鏈接庫中對應的函數功能相同。如果定義了UNICODE標識符,那么這些函數將接受寬字符串,否則只接受常規字符串。寬字符串版的lstrlenW函數可在Windows 98中執行。

有文字模式、命令列C語言程序寫作歷史的程序寫作者往往特別喜歡printf函數。即使可以使用更簡單的命令(例如puts),但printf出現在Kernighan和Ritchie的「hello, world」程序中一點也不會令人驚奇。我們知道,增強后的「hello, world」最終還是需要printf的格式化輸出,因此我們最好從頭開始就使用它。

但有個壞消息:在Windows程序中不能使用printf。雖然Windows程序中可以使用大多數C的執行時期鏈接庫-實際上,許多程序寫作者更愿意使用C內存管理和文件I/O函數而不是Windows中等效的函數-Windows對標準輸入和標準輸出沒有概念。在Windows程序中可使用fprintf,而不是printf。

還有一個好消息,那就是仍然可以使用sprintf及sprintf系列中的其它函數來顯示文字。這些函數除了將內容格式化輸出到函數第一個參數所提供的字符串緩沖區以外,其功能與printfI相同。然后便可對該字符串進行操作(例如將其傳給MessageBox)。

如果您從未使用過sprintf (我第一次開始寫Windows程序時也沒用過此函數),這里有一個簡短的執行實體,printf函數說明如下:

int printf (const char * szFormat, ...) ;

第一個參數是一個格式字符串,后面是與格式字符串中的代碼相對應的不同類型多個參數。

sprintf函數定義如下:

int sprintf (char * szBuffer, const char * szFormat, ...) ;

第一個參數是字符緩沖區;后面是一個格式字符串。Sprintf不是將格式化結果標準輸出,而是將其存入szBuffer。該函數返回該字符串的長度。在文字模式程序設計中,

printf ("The sum of %i and %i is %i", 5, 3, 5+3) ;

的功能相同于

char szBuffer [100] ;sprintf (szBuffer, "The sum of %i and %i is %i", 5, 3, 5+3) ;puts (szBuffer) ;

在Windows中,使用MessageBox顯示結果優于puts。

幾乎每個人都經歷過,當格式字符串與被格式化的變量不合時,可能使printf執行錯誤并可能造成程序當掉。使用sprintf時,您不但要擔心這些,而且還有一個新的負擔:您定義的字符串緩沖區必須足夠大以存放結果。Microsoft專用函數_snprintf解決了這一問題,此函數引進了另一個參數,表示以字符計算的緩沖區大小。

vsprintf是sprintf的一個變形,它只有三個參數。vsprintf用于執行有多個參數的自訂函數,類似printf格式。vsprintf的前兩個參數與sprintf相同:一個用于保存結果的字符緩沖區和一個格式字符串。第三個參數是指向格式化參數數組的指針。實際上,該指針指向在堆棧中供函數呼叫的變量。va_list、va_start和va_end宏(在STDARG.H中定義)幫助我們處理堆棧指針。本章最后的SCRNSIZE程序展示了使用這些宏的方法。使用vsprintf函數,sprintf函數可以這樣編寫:

int sprintf (char * szBuffer, const char * szFormat, ...){int     iReturn ;va_list pArgs ;va_start (pArgs, szFormat) ;iReturn = vsprintf (szBuffer, szFormat, pArgs) ;va_end (pArgs) ;return iReturn ;}

va_start宏將pArg設置為指向一個堆棧變量,該變量地址在堆棧參數szFormat的上面。

由于許多Windows早期程序使用了sprintf和vsprintf,最終導致Microsoft向Windows API中增添了兩個相似的函數。Windows的wsprintf和wvsprintf函數在功能上與sprintf和vsprintf相同,但它們不能處理浮點格式。

當然,隨著寬字符的發表,sprintf類型的函數增加許多,使得函數名稱變得極為混亂。表2-1列出了Microsoft的C執行時期鏈接庫和Windows支持的所有sprintf函數。


表2-1


ASCII

寬字符

常規

參數的變數個數




標準版

sprintf

swprintf

_stprintf

最大長度版

_snprintf

_snwprintf

_sntprintf

Windows版

wsprintfA

wsprintfW

wsprintf

參數數組的指針




標準版

vsprintf

vswprintf

_vstprintf

最大長度版

_vsnprintf

_vsnwprintf

_vsntprintf

Windows版

wvsprintfA

wvsprintfW

wvsprintf

在寬字符版的sprintf函數中,將字符串緩沖區定義為寬字符串。在寬字符版的所有這些函數中,格式字符串必須是寬字符串。不過,您必須確保傳遞給這些函數的其它字符串也必須由寬字符組成。

程序2-1所示的SCRNSIZE程序展示了如何實作MessageBoxPrintf函數,該函數有許多參數并能像printf那樣編排它們的格式。

程序2-1  SCRNSIZESCRNSIZE.C/*---------------------------------------------------------------------------SCRNSIZE.C -- Displays screen size in a message box(c) Charles Petzold, 1998----------------------------------------------------------------------------*/#include <windows.h>#include <tchar.h>   #include <stdio.h>   int CDECL MessageBoxPrintf (TCHAR * szCaption, TCHAR * szFormat, ...){TCHAR   szBuffer [1024] ;va_list pArgList ;// The va_start macro (defined in STDARG.H) is usually equivalent to:// pArgList = (char *) &szFormat + sizeof (szFormat) ;va_start (pArgList, szFormat) ;// The last argument to wvsprintf points to the arguments_vsntprintf ( szBuffer, sizeof (szBuffer) / sizeof (TCHAR),szFormat, pArgList) ;// The va_end macro just zeroes out pArgList for no good reasonva_end (pArgList) ;return MessageBox (NULL, szBuffer, szCaption, 0) ;}int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow){int cxScreen, cyScreen ;cxScreen = GetSystemMetrics (SM_CXSCREEN) ;cyScreen = GetSystemMetrics (SM_CYSCREEN) ;MessageBoxPrintf (    TEXT ("ScrnSize"),TEXT ("The screen is %i pixels wide by %i pixels high."),cxScreen, cyScreen) ;return 0 ;}

經由從GetSystemMetrics函數得到的信息,該程序以圖素為單位顯示了視訊顯示的寬度和高度。GetSystemMetrics是一個能用來獲得Windows中不同對象的尺寸信息的函數。事實上,我將在第四章用GetSystemMetrics函數向您展示如何在一個Windows窗口中顯示和滾動多行文字。

為國際市場準備的Windows程序不光要使用Unicode。國際化超出了本書的范圍,但在Nadine Kano所寫的《Developing International Software for Windows 95 and Windows NT》(Microsoft Press,1995年)一書中涉獵了許多。

本書中的程序寫作時被限制成既可使用也可不使用定義的UNICODE標識符來編譯。這包括對所有字符和字符串定義使用TCHAR,對字符串文字使用TEXT宏,以及注意不要混淆字節和字符。例如,注意SCRNSIZE中的 _vsntprintf呼叫。第二個參數是緩沖區的字符大小。通常,您使用sizeof (szBuffer)。但如果緩沖區中有寬字符,則返回的不是緩沖區的字符長度,而是緩沖區的字節大小。您必須用sizeof(TCHAR)將其分開。

通常,在Visual C++ Developer Studio中,可使用兩種不同的設定來編譯程序:Debug和Release。為簡便起見,對本書的范例程序,我已修改了Debug設定,以便于定義UNICODE標識符。如果程序使用了需要字符串作參數的C鏈接庫函數,那么_UNICODE標識符也在Debug設定中定義(要了解這是在哪里完成的,請從「Project」菜單中選擇「Settings」,然后單擊「C/C++」標簽)。使用這種方式,這些程序就可以方便地被重新編譯和連結以供測試。

本書中所有程序-無論是否為Unicode編譯-都可以在Windows NT下執行。只有極少數情況例外。本書中按Unicode編譯的程序不能在Windows 98中執行,而非Unicode版則可以。本章和第一章的程序就是兩個特例。MessageBoxW是Windows 98支持的少數寬字符Windows函數之一。在SCRNSIZE.C中,如果用Windows函數wprintf代替了_vsntprintf(您還必須刪除該函數的第二個參數),那么SCRNSIZE.C的Unicode版將不能在Windows 98下執行,這是因為Windows 98不支持wprintfW。

在本書的后面(特別在第六章,介紹鍵盤的使用時),我們將看到,編寫能處理遠東版Windows雙字符集的Windows程序不是一件容易的事情。本書沒有說明如何去做,并且基于這個原因,本書中的某些非Unicode版本的程序在遠東版的Windows下不能正常執行。這也是Unicode對將來的程序設計如此重要的一條理由。Unicode允許程序更容易地跨越國界。



本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/450280.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/450280.shtml
英文地址,請注明出處:http://en.pswp.cn/news/450280.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

webpack4.x 模塊化淺析-CommonJS

先看下webpack官方文檔中對模塊的描述&#xff1a; 在模塊化編程中&#xff0c;開發者將程序分解成離散功能塊(discrete chunks of functionality)&#xff0c;并稱之為模塊。每個模塊具有比完整程序更小的接觸面&#xff0c;使得校驗、調試、測試輕而易舉。 精心編寫的模塊提供…

設計模式--抽象工廠(個人筆記)

一、抽象工廠的應用場景以及優缺點 1 應用場景&#xff1a; 如果系統需要多套的代碼解決方案&#xff0c;并且每套的代碼解決方案中又有很多相互關聯的產品類型&#xff0c;并且在系統中我們可以相互替換的使用一套產品的時候可以使用該模式&#xff0c;客戶端不需要依賴具體的…

利用阿里云OSS對文件進行存儲,上傳等操作

--pom.xml加入阿里OSS存儲依賴 <!--阿里云OSS存儲--> <dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>2.8.3</version> </dependency> --配置阿里云oss相關常量參數 /…

Java并發編程之ThreadGroup

ThreadGroup是Java提供的一種對線程進行分組管理的手段&#xff0c;可以對所有線程以組為單位進行操作&#xff0c;如設置優先級、守護線程等。 線程組也有父子的概念&#xff0c;如下圖&#xff1a; 線程組的創建 1 public class ThreadGroupCreator {2 3 public static v…

springboot 緩存ehcache的簡單使用

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 步驟&#xff1a; 1. pom文件中加 maven jar包&#xff1a; <!-- ehcache 緩存 --><dependency><groupId>net.sf.eh…

Spring boot + mybatis plus 快速構建項目,生成基本業務操作代碼。

---進行業務建表&#xff0c;這邊根據個人業務分析&#xff0c;不具體操作 --加入mybatis plus pom依賴 <!-- mybatis-plus 3.0.5--> <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId>&l…

給手機瀏覽器減負 輕裝上陣才能速度制勝

隨著手機瀏覽器的發展&#xff0c;瀏覽器已經變得臃腫不堪&#xff0c;各種“功能”系于一身&#xff0c;有廣告、社區、樂園等等&#xff0c;我們真的需要它們嗎&#xff1f;如何才能讓瀏覽器做到輕裝上陣&#xff0c;又能高效滿足我們需求呢&#xff1f; 過多“功能”的瀏覽器…

653. Two Sum IV - Input is a BST

題目來源&#xff1a; 自我感覺難度/真實難度&#xff1a; 題意&#xff1a; 分析&#xff1a; 自己的代碼&#xff1a; class Solution(object):def findTarget(self, root, k):""":type root: TreeNode:type k: int:rtype: bool"""Allself.InO…

解決 dubbo問題:Forbid consumer 192.xx.xx.1 access service com.xx.xx.xx.rpc.api.xx from registry 116.xx1

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 我的情況是&#xff1a; 原本我把服務放在A工程中&#xff0c;后來改到B工程中了&#xff0c;所以原來的服務不存在了&#xff0c;查不…

vue學習:7、路由跳轉

2019獨角獸企業重金招聘Python工程師標準>>> <body><div id"app"></div></body><script type"text/javascript">var Login {template: <div>我是登陸界面</div>};var Register {template: <div…

Spring Retry 重試機制實現及原理

概要 Spring實現了一套重試機制&#xff0c;功能簡單實用。Spring Retry是從Spring Batch獨立出來的一個功能&#xff0c;已經廣泛應用于Spring Batch,Spring Integration, Spring for Apache Hadoop等Spring項目。本文將講述如何使用Spring Retry及其實現原理。 背景 重試&…

inline 內聯函數詳解 內聯函數與宏定義的區別

一、在C&C中   一、inline 關鍵字用來定義一個類的內聯函數&#xff0c;引入它的主要原因是用它替代C中表達式形式的宏定義。表達式形式的宏定義一例&#xff1a;#define ExpressionName(Var1,Var2) ((Var1)(Var2))*((Var1)-(Var2))為什么要取代這種形式呢&#xff0c;且…

Oracle序列更新為主鍵最大值

我們在使用 Oracle 數據庫的時候&#xff0c;有時候會選擇使用自增序列作為主鍵。但是在開發過程中往往會遇到一些不規范的操作&#xff0c;導致表的主鍵值不是使用序列插入的。這樣在數據移植的時候就會出現各種各樣的問題。當然數據庫主鍵不使用序列是一種很好的方式&#xf…

dubbo forbid service的解決辦法

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 017-05-31 10:36:54.523 [http-nio-8080-exec-5] ERROR c.h.pdl.web.APIExceptionHandler - Unknown Exception, URI /payday-loan-co…

用SSH登錄遠程的機器,在遠程機器上執行本地機器上的腳本

假設本地的機器IP為10.245.111.90&#xff0c;我們想要在10.245.111.93上執行一個保存在10.245.111.90上的腳本。經過測試通過的命令如下&#xff1a;ssh root10.245.111.93 bash -s < /root/testlocal.sh如果要帶參數的話&#xff0c;那就需要參考這篇文章中描述的代碼了。…

golang學習之旅(1)

這段時間我開始了golang語言學習&#xff0c;其實也是為了個人的職業發展的拓展和衍生&#xff0c;語言只是工具&#xff0c;但是每個語言由于各自的特點和優勢&#xff0c;golang對于當前編程語言的環境&#xff0c;是相對比較新的語言&#xff0c;對于區塊鏈&#xff0c;大數…

為什么要在Linux平臺上學C語言?用Windows學C語言不好嗎?

用Windows還真的是學不好C語言。C語言是一種面向底層的編程語言&#xff0c;要寫好C程序&#xff0c;必須對操作系統的工作原理非常清楚&#xff0c;因為操作系統也是用C寫的&#xff0c;我們用C寫應用程序直接使用操作系統提供的接口&#xff0c;Linux是一種開源的操作系統&am…

數據庫中Schema(模式)概念的理解

在學習SQL的過程中&#xff0c;會遇到一個讓你迷糊的Schema的概念。實際上&#xff0c;schema就是數據庫對象的集合&#xff0c;這個集合包含了各種對象如&#xff1a;表、視圖、存儲過程、索引等。為了區分不同的集合&#xff0c;就需要給不同的集合起不同的名字&#xff0c;默…

linux系統中打rz命令后出現waiting to receive.**B0100000023be50

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 linux系統中打rz命令后出現 waiting to receive.**B0100000023be50 而沒有出現選擇文件彈出框是什么問題&#xff1a; 我本來用的是 gi…

golang學習之旅(2)- go的數據基本數據類型及變量定義方式

叮鈴鈴&#xff0c;這不有人在評論問下一篇何時更新&#xff0c;這不就來了嘛&#xff0c;&#x1f604; 今天我們說說golang 的基本數據類型 基本類型如下&#xff1a; //基本類型 布爾類型&#xff1a;bool 即true 、flase 類似于java中的boolean 字符類型&#xff1a;s…