第一部分 程序員必讀
第1章 對程序錯誤的處理
在開(kāi)始介紹Microsoft Windows 的特性之前,必須首先了解Windows的各個(gè)函數是如何進(jìn)行錯誤處理的。
當調用一個(gè)Windows函數時(shí),它首先要檢驗傳遞給它的的各個(gè)參數的有效性,然后再設法執行任務(wù)。如果傳遞了一個(gè)無(wú)效參數,或者由于某種原因無(wú)法執行這項操作,那么操作系統就會(huì )返回一個(gè)值,指明該函數在某種程度上運行失敗了。表1 - 1列出了大多數Windows函數使用的返回值的數據類(lèi)型。
表1-1 Wi n d o w s 函數常用的返回值類(lèi)型
數據類(lèi)型 表示失敗的值
V O I D 該函數的運行不可能失敗。Wi n d o w s 函數的返回值類(lèi)型很少是V O I D
B O O L 如果函數運行失敗,那么返回值是0 ,否則返回的是非0 值。最好對返回值進(jìn)行測試,以確定它是0 還是非0 。不要測試返回值是否為T(mén) R U E
H A N D L E 如果函數運行失敗,則返回值通常是N U L L ,否則返回值為H A N D L E ,用于標識你可以操作的一個(gè)對象。注意,有些函數會(huì )返回一個(gè)句柄值I N VALID_ HANDLE_VA L U E ,它被定義為- 1 。函數的Platform SDK 文檔將會(huì )清楚地說(shuō)明該函數運行失敗時(shí)返回的是N U L L 還是I N VA L I D _ H A N D L E _ VA L I D
P V O I D 如果函數運行失敗,則返回值是N U L L ,否則返回P V O I D ,以標識數據塊的內存地址
L O N G / D W O R D 這是個(gè)難以處理的值。返回數量的函數通常返回L O N G 或D W O R D 。如果由于某種原因,函數無(wú)法對想要進(jìn)行計數的對象進(jìn)行計數,那么該函數通常返回0 或- 1 (根據函數而定)。如果調用的函數返回了L O N G / D W O R D ,那么請認真閱讀Platform SDK文檔,以確保能正確檢查潛在的錯誤
一個(gè)Wi n d o w s 函數返回的錯誤代碼對了解該函數為什么會(huì )運行失敗常常很有用。M i c r o s o f t公司編譯了一個(gè)所有可能的錯誤代碼的列表,并且為每個(gè)錯誤代碼分配了一個(gè)3 2 位的號碼。
從系統內部來(lái)講,當一個(gè)Wi n d o w s 函數檢測到一個(gè)錯誤時(shí),它會(huì )使用一個(gè)稱(chēng)為線(xiàn)程本地存儲器(thread-local storage )的機制,將相應的錯誤代碼號碼 與調用的線(xiàn)程關(guān)聯(lián)起來(lái)(線(xiàn)程本地存儲器將在第2 1 章中介紹)。這將使線(xiàn)程能夠互相獨立地運行,而不會(huì )影響各自的錯誤代碼。當函數返回時(shí),它的返回值 就能指明一個(gè)錯誤已經(jīng)發(fā)生。若要確定這是個(gè)什么錯誤,請調用G e t L a s t E r r o r 函數:
DWORD GetLastError();
該函數只返回線(xiàn)程的3 2 位錯誤代碼。
當你擁有3 2 位錯誤代碼的號碼時(shí),必須將該號碼轉換成更有用的某種對象。Wi n E r r o r. h 頭文件包含了M i c r o s o f t 公司定義的錯誤代碼的列 表。下面顯示了該列表的某些內容,使你能夠看到它的大概樣子:
// MessageId: ERROR_SUCCESS
//
// MessageText:
//
// The operation completed successfully.
//
#define ERROR_SUCCESS 0L
#define NO_ERROR 0L // dderror
//
// MessageId: ERROR_INVALID_FUNCTION
//
// MessageText:
//
// Incorrect function.
//
#define ERROR_INVALID_FUNCTION 1L // dderror
//
// MessageId: ERROR_FILE_NOT_FOUND
//
// MessageText:
//
// The system cannot find the file specified.
//
#define ERROR_FILE_NOT_FOUND 2L
//
// MessageId: ERROR_PATH_NOT_FOUND
//
// MessageText:
//
// The system cannot find the path specified.
//
#define ERROR_PATH_NOT_FOUND 3L
//
// MessageId: ERROR_TOO_MANY_OPEN_FILES
//
// MessageText:
//
// The system cannot open the file.
//
#define ERROR_TOO_MANY_OPEN_FILES 4L
//
// MessageId: ERROR_ACCESS_DENIED
//
// MessageText:
//
// Access is denied.
//
#define ERROR_ACCESS_DENIED 5L
如你所見(jiàn),每個(gè)錯誤都有3 種表示法:一個(gè)消息I D (這是你可以在源代碼中使用的一個(gè)宏,以便與G e t L a s t E r r o r 的返回值進(jìn)行比較),消息文本(對錯誤的英文描述)和一個(gè)號碼(應該避免使用這個(gè)號碼,可使用消息I D )。請記住,這里只顯示了Wi n E r r o r. h 頭文件中的很少一部分內容,整個(gè)文件的長(cháng)度超過(guò)2 1 0 0 0 行。
當Wi n d o w s 函數運行失敗時(shí),應該立即調用G e t L a s t E r r o r 函數。如果調用另一個(gè)Wi n d o w s 函數,它的值很可能被改寫(xiě)。
注意G e t L a s t E r r o r 能返回線(xiàn)程產(chǎn)生的最后一個(gè)錯誤。如果該線(xiàn)程調用的Wi n d o w s 函數運行成功,那么最后一個(gè)錯誤代碼就不被改寫(xiě),并且不指明運行成功。有少數Wi n d o w s 函數并不遵循這一規則,它會(huì )更改最后的錯誤代碼;但是Platform SDK 文檔通常指明,當函數運行成功時(shí),該函數會(huì )更改最后的錯誤代碼。
Wi n d o w s 9 8 許多Windows 98 的函數實(shí)際上是用M i c r o s o f t 公司的1 6 位Windows 3.1 產(chǎn)品產(chǎn)生的1 6 位代碼來(lái)實(shí)現的。這種比較老的代碼并 不通過(guò)G e t L a s t E r r o r 之類(lèi)的函數來(lái)報告錯誤,而且M i c r o s o f t 公司并沒(méi)有在Windows 98 中修改1 6 位代碼,以支持這種錯誤處理方式 。對于我們來(lái)說(shuō),這意味著(zhù)Windows 98 中的許多Wi n 3 2 函數在運行失敗時(shí)不能設置最后的錯誤代碼。該函數將返回一個(gè)值,指明運行失敗,這樣你就能夠 發(fā)現該函數確實(shí)已經(jīng)運行失敗,但是你無(wú)法確定運行失敗的原因。
有些Wi n d o w s 函數之所以能夠成功運行,其中有許多原因。例如,創(chuàng )建指明的事件內核對象之所以能夠取得成功,是因為你實(shí)際上創(chuàng )建了該對象,或者因為已經(jīng)存在帶有相同名字的事件內核對象。你應搞清楚成功的原因。為了將該信息返回,M i c r o s o f t 公司選擇使用最后錯誤代碼機制。這樣,當某些函數運行成功時(shí),就能夠通過(guò)調用G e t L a d t E r r o r 函數來(lái)確定其他的一些信息。對于具有這種行為特性的函數來(lái)說(shuō),Platform SDK 文檔清楚地說(shuō)明了G e t L a s t E r r o r 函數可以這樣使用。請參見(jiàn)該文檔,找出C r e a t e E v e n t 函數的例子。
進(jìn)行調試的時(shí)候,監控線(xiàn)程的最后錯誤代碼是非常有用的。在Microsoft Visual studio 6.0 中,M i c r o s o f t 的調試程序支持一個(gè)非常有用的特性,即可以配置Wa t c h 窗口,以便始終都能顯示線(xiàn)程的最后錯誤代碼的號碼和該錯誤的英文描述。通過(guò)選定Wa t c h 窗口中的一行,并鍵入“@ e r r, h r ”,就能夠做到這一點(diǎn)。觀(guān)察圖1 - 1 ,你會(huì )看到已經(jīng)調用了C r e a t e F i l e 函數。該函數返回I N VA L I D _ H A N D L E _ VA L U E (- 1 )的H A N D L E ,表示它未能打開(kāi)指定的文件。但是Wa t c h 窗口向我們顯示最后錯誤代碼(即如果調用G e t L a s t E r r o r 函數,該函數返回的錯誤代碼)是0 x 0 0 0 0 0 0 0 2 。該Wa t c h 窗口又進(jìn)一步指明錯誤代碼2 是指“系統不能找到指定的文件。”你會(huì )發(fā)現它與Wi n E r r o r. h 頭文件中的錯誤代碼2 所指的字符串是相同的。
圖1-1 在Visual Studio 6.0 的Wa t c h 窗口中鍵入“@ e r r, h r ”,就可以查看當前線(xiàn)程的最后錯誤代碼
Visual studio 還配有一個(gè)小的實(shí)用程序,稱(chēng)為Error Lookup ??梢允褂肊rror Lookup將錯誤代碼的號碼轉換成相應文本描述(見(jiàn)圖1 - 2 )。
圖1-2 Error Lookup 窗口
如果在編寫(xiě)的應用程序中發(fā)現一個(gè)錯誤,可能想要向用戶(hù)顯示該錯誤的文本描述。Wi n d o w s 提供了一個(gè)函數,可以將錯誤代碼轉換成它的文本描述。該函數稱(chēng)為FormatMessage,顯示如下:
DWORD FormatMessage(
DWORD dwFlags, // source and processing options
LPCVOID lpSource, // pointer to message source
DWORD dwMessageId, // requested message identifier
DWORD dwLanguageId, // language identifier for requested message
LPTSTR lpBuffer, // pointer to message buffer
DWORD nSize, // maximum size of message buffer
va_list *Arguments // pointer to array of message inserts
);
F o r m a t M e s s a g e 函數的功能實(shí)際上是非常豐富的,在創(chuàng )建向用戶(hù)顯示的字符串信息時(shí),它是首選函數。該函數之所以有這樣大的作用,原因之一 是它很容易用多種語(yǔ)言進(jìn)行操作。該函數能夠檢測出用戶(hù)首選的語(yǔ)言(在Regional Settings Control Panel 小應用程序中設定),并返回相應的文本。當然 ,首先必須自己轉換字符串,然后將已轉換的消息表資源嵌入你的. e x e 文件或D L L 模塊中,然后該函數會(huì )選定正確的嵌入對象。E r r o r S h o w 示 例應用程序(本章后面將加以介紹)展示了如何調用該函數,以便將M i c r o s o f t 公司定義的錯誤代碼轉換成它的文本描述。
有些人常常問(wèn)我,M i c r o s o f t 公司是否建立了一個(gè)主控列表,以顯示每個(gè)Wi n d o w s 函數可能返回的所有錯誤代碼??上?,回答是沒(méi)有這樣的列 表,而且M i c r o s o f t 公司將永遠不會(huì )建立這樣的一個(gè)列表。因為在創(chuàng )建系統的新版本時(shí),建立和維護該列表實(shí)在太困難了。
建立這樣一個(gè)列表存在的問(wèn)題是,你可以調用一個(gè)Wi n d o w s 函數,但是該函數能夠在內部調用另一個(gè)函數,而這另一個(gè)函數又可以調用另一個(gè)函數,如 此類(lèi)推。由于各種不同的原因,這些函數中的任何一個(gè)函數都可能運行失敗。有時(shí),當一個(gè)函數運行失敗時(shí),較高級的函數對它進(jìn)行恢復,并且仍然可以執行 你想執行的操作。為了創(chuàng )建該主控列表,M i c r o s o f t 公司必須跟蹤每個(gè)函數的運行路徑,并建立所有可能的錯誤代碼的列表。這項工作很困難。而且 ,當創(chuàng )建系統的新版本時(shí),這些函數的運行路徑還會(huì )改變。
1.1 定義自己的錯誤代碼
前面已經(jīng)說(shuō)明Wi n d o w s 函數是如何向函數的調用者指明發(fā)生的錯誤,你也能夠將該機制用于自己的函數。比如說(shuō),你編寫(xiě)了一個(gè)希望其他人調用的函數,你的函數可能因為這樣或那樣的原因而運行失敗,你必須向函數的調用者說(shuō)明它已經(jīng)運行失敗。
若要指明函數運行失敗,只需要設定線(xiàn)程的最后的錯誤代碼,然后讓你的函數返回FA L S E 、I N VA L I D _ H A N D L E _ VA L U E 、N U L L 或者返回任何合適的信息。若要設定線(xiàn)程的最后錯誤代碼,只需調用下面的代碼:
請將你認為合適的任何3 2 位號碼傳遞給該函數。嘗試使用Wi n E r r o r. h 中已經(jīng)存在的代碼,
VOID SetLastError(DWORD dwErrCode);
只要該代碼能夠正確地指明想要報告的錯誤即可。如果你認為Wi n E r r o r. h 中的任何代碼都不能正確地反映該錯誤的性質(zhì),那么可以創(chuàng )建你自己的代碼 。錯誤代碼是個(gè)3 2 位的數字,劃分成表1-2所示的各個(gè)域。
表1-2 錯誤代碼的域
位 3 1 ~30 29 28 27~16 15~0
內容 嚴重性 M i c r o s o f t/客戶(hù) 保留 設備代碼 異常代碼
含義 0 =成功 0 =M i c r o s o f t公司定義的代碼 必須是0 由M i c r o s o f t公司定義 由Microsoft/客戶(hù)定義
1 =供參考 1 =客戶(hù)定義的代碼
2 =警告
3 =錯誤
這些域將在第2 4 章中詳細講述?,F在,需要知道的重要域是第2 9 位。M i c r o s o f t 公司規定,他們建立的所有錯誤代碼的這個(gè)信息位均使用0 。如果創(chuàng )建自己的錯誤代碼,必須使2 9 位為1 。這樣,就可以確保你的錯誤代碼與M i c r o s o f t 公司目前或者將來(lái)定義的錯誤代碼不會(huì )發(fā)生沖突。
1.2 ErrorShow示例應用程序
E r r o r S h o w 應用程序“01 ErrorShow. e x e ”(在清單1 - 1 中列出)展示了如何獲取錯誤代碼的文本描述的方法。該應用程序的源代碼和資源文件位于本書(shū)所附光盤(pán)上的0 1 - E r r o r S h o w 目錄下。一般來(lái)說(shuō),該應用程序用于顯示調試程序的Wa t c h 窗口和Error Lookup 程序是如何運行的。當啟動(dòng)該程序時(shí),就會(huì )出現如圖1 - 3 所示的窗口。
圖1-3 Error Show 窗口
可以將任何錯誤代碼鍵入該編輯控件。當單擊Look up 按鈕時(shí),在底部的滾動(dòng)窗口中就會(huì )顯示該錯誤的文本描述。該應用程序唯一令人感興趣的特性是如何調用F o r m a t M e s s a g e 函數。下面是使用該函數的方法:
//Get the error code
DWORD dwError = GetDlgItemInt(hwnd, IDC_ERRORCODE, NULL, FALSE);
//Buffer that gets the error message string
HLOCAL hlocal = NULL;
//Get the error code's textual description
BOOL fOk = FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER,
NULL, dwError, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
(PTSTR)&hlocal, 0, NULL);
.
.
.
if (hlocal != NULL)
{
SetDlgItemText(hwnd, IDC_ERRORTEXT, (PCTSTR)LocalLock(hlocal));
LocalFree(hlocal);
}
else
SetDlgItemText(hwnd, IDC_ERRORTEXT,
TEXT("Error number not found."));
第一個(gè)代碼行用于從編輯控件中檢索錯誤代碼的號碼。然后,建立一個(gè)內存塊的句柄并將它初始化為N U L L 。F o r m a t M e s s a g e 函數在內部對內存塊進(jìn)行分配,并將它的句柄返回給我們。
當調用F o r m a t M e s s a g e 函數時(shí),傳遞了F O R M AT _ M E S S A G E _ F R O M _ S Y S T E M 標志。該標志告訴F o r m a t M e s s a g e 函數,我們想要系統定義的錯誤代碼的字符串。還傳遞了F O R M AT _M E S S A G E _ A L L O C AT E _ B U F F E R 標志,告訴該函數為錯誤代碼的文本描述分配足夠大的內存塊。該內存塊的句柄將在h l o c a l 變量中返回。第三個(gè)參數指明想要查找的錯誤代碼的號碼,第四個(gè)參數指明想要文本描述使用什么語(yǔ)言。
如果F o r m a t M e s s a g e 函數運行成功,那么錯誤代碼的文本描述就位于內存塊中,將它拷貝到對話(huà)框底部的滾動(dòng)窗口中。如果F o r m a t M e s s a g e 函數運行失敗,設法查看N e t M s g . d l l 模塊中的消息代碼,以了解該錯誤是否與網(wǎng)絡(luò )有關(guān)。使用N e t M s g . d l l 模塊的句柄,再次調用F o r m a t M e s s a g e 函數。你會(huì )看到,每個(gè)D L L (或. e x e )都有它自己的一組錯誤代碼,可以使用Message Compiler (M C . e x e )將這組錯誤代碼添加給該模塊,并將一個(gè)資源添加給該模塊。這就是Visual Studio 的Error Lookup 工具允許你用M o d u l e s對話(huà)框進(jìn)行的操作。以下是清單1 - 1E r r o r S h o w 示例應用程序。
清單1-1 ErrorShow 示例應用程序
/******************************************************************************
Module: ErrorShow.cpp
Notices: Copyright (c) 2000 Jeffrey Richter
******************************************************************************/
#i nclude "..\CmnHdr.h" /* See Appendix A. */
#i nclude <Windowsx.h>
#i nclude <tchar.h>
#i nclude "Resource.h"
///////////////////////////////////////////////////////////////////////////////
#define ESM_POKECODEANDLOOKUP (WM_USER + 100)
const TCHAR g_szAppName[] = TEXT("Error Show");
///////////////////////////////////////////////////////////////////////////////
BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam)
{
chSETDLGICONS(hwnd, IDI_ERRORSHOW);
// Don't accept error codes more than 5 digits long
Edit_LimitText(GetDlgItem(hwnd, IDC_ERRORCODE), 5);
// Look up the command-line passed error number
SendMessage(hwnd, ESM_POKECODEANDLOOKUP, lParam, 0);
return(TRUE);
}
///////////////////////////////////////////////////////////////////////////////
void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
switch (id)
{
case IDCANCEL:
EndDialog(hwnd, id);
break;
case IDC_ALWAYSONTOP:
SetWindowPos(hwnd, IsDlgButtonChecked(hwnd, IDC_ALWAYSONTOP)
HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
break;
case IDC_ERRORCODE:
EnableWindow(GetDlgItem(hwnd, IDOK), Edit_GetTextLength(hwndCtl) > 0);
break;
case IDOK:
// Get the error code
DWORD dwError = GetDlgItemInt(hwnd, IDC_ERRORCODE, NULL, FALSE);
HLOCAL hlocal = NULL; // Buffer that gets the error message string
// Get the error code's textual description
BOOL fOk = FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER,
NULL, dwError, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
(PTSTR) &hlocal, 0, NULL);
if (!fOk)
{
// Is it a network-related error?
HMODULE hDll = LoadLibraryEx(TEXT("netmsg.dll"), NULL,
DONT_RESOLVE_DLL_REFERENCES);
if (hDll != NULL)
{
FormatMessage(
FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_FROM_SYSTEM,
hDll, dwError, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
(PTSTR) &hlocal, 0, NULL);
FreeLibrary(hDll);
}
}
if (hlocal != NULL)
{
SetDlgItemText(hwnd, IDC_ERRORTEXT, (PCTSTR) LocalLock(hlocal));
LocalFree(hlocal);
}
else
{
SetDlgItemText(hwnd, IDC_ERRORTEXT, TEXT("Error number not found."));
}
break;
}
}
///////////////////////////////////////////////////////////////////////////////
INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand);
case ESM_POKECODEANDLOOKUP:
SetDlgItemInt(hwnd, IDC_ERRORCODE, (UINT) wParam, FALSE);
FORWARD_WM_COMMAND(hwnd, IDOK, GetDlgItem(hwnd, IDOK), BN_CLICKED,
PostMessage);
SetForegroundWindow(hwnd);
break;
}
return(FALSE);
}
///////////////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int)
{
HWND hwnd = FindWindow(TEXT("#32770"), TEXT("Error Show"));
if (IsWindow(hwnd))
{
// An instance is already running, activate it and send it the new #
SendMessage(hwnd, ESM_POKECODEANDLOOKUP, _ttoi(pszCmdLine), 0);
}
else
{
DialogBoxParam(hinstExe, MAKEINTRESOURCE(IDD_ERRORSHOW),
NULL, Dlg_Proc, _ttoi(pszCmdLine));
}
return(0);
}
//////////////////////////////// End of File //////////////////////////////////
//ErrorShow.rc Microsoft Developer Studio generated resource script.
//
#i nclude "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#i nclude "afxres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (U.S.) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32
/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//
IDD_ERRORSHOW DIALOGEX 0, 0, 182, 42
STYLE DS_SETFOREGROUND | DS_3DLOOK | DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE |
WS_CAPTION | WS_SYSMENU
CAPTION "Error Show"
FONT 8, "MS Sans Serif"
BEGIN
LTEXT "Error:",IDC_STATIC,4,4,19,8
EDITTEXT IDC_ERRORCODE,24,2,24,14,ES_AUTOHSCROLL | ES_NUMBER
DEFPUSHBUTTON "Look up",IDOK,56,2,36,14
CONTROL "&On top",IDC_ALWAYSONTOP,"Button",BS_AUTOCHECKBOX |
WS_TABSTOP,104,4,38,10
EDITTEXT IDC_ERRORTEXT,4,20,176,20,ES_MULTILINE | ES_AUTOVSCROLL |
ES_READONLY | NOT WS_BORDER | WS_VSCROLL,
WS_EX_CLIENTEDGE
END
/////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
//
#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO DISCARDABLE
BEGIN
IDD_ERRORSHOW, DIALOG
BEGIN
LEFTMARGIN, 7
RIGHTMARGIN, 175
TOPMARGIN, 7
BOTTOMMARGIN, 35
END
END
#endif // APSTUDIO_INVOKED
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE DISCARDABLE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE DISCARDABLE
BEGIN
"#i nclude ""afxres.h""\r\n"
"\0"
END
3 TEXTINCLUDE DISCARDABLE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_ERRORSHOW ICON DISCARDABLE "ErrorShow.ico"
#endif // English (U.S.) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED
第2章 U n i c o d e
隨著(zhù)M i c r o s o f t 公司的Wi n d o w s 操作系統在全世界日益廣泛的流行,對于軟件開(kāi)發(fā)人員來(lái)說(shuō),將目標瞄準國際上的各個(gè)不同市場(chǎng),已經(jīng)成為一個(gè)越來(lái)越重要的問(wèn)題。美國的軟件版本比國際版本提前6 個(gè)月推向市場(chǎng),這曾經(jīng)是個(gè)司空見(jiàn)慣的現象。但是,由于各國對Wi n d o w s 操作系統提供了越來(lái)越多的支持,因此就更加容易為國際市場(chǎng)生產(chǎn)各種應用軟件,從而縮短了軟件的美國版本與國際版本推出的時(shí)間間隔。
Wi n d o w s 操作系統始終不逾地提供各種支持,以幫助軟件開(kāi)發(fā)人員進(jìn)行應用程序的本地化工作。應用軟件可以從各種不同的函數中獲得特定國家的信息,并可觀(guān)察控制面板的設置,以確定用戶(hù)的首選項。Wi n d o w s 甚至支持不同的字體,以適應應用的需要。
之所以將這一章放在本書(shū)的開(kāi)頭,是因為考慮到U n i c o d e 是開(kāi)發(fā)任何應用程序時(shí)要采用的基本步驟。本書(shū)的每一章中幾乎都要講到關(guān)于U n i c o d e 的問(wèn)題,而且書(shū)中給出的所有示例應用程序都是“用U n i c o d e 實(shí)現的”。如果你為Microsoft Windows 2000 或Microsoft Windows CE 開(kāi)發(fā)應用程序,你應該使用U n i c o d e 進(jìn)行開(kāi)發(fā)。如果你為Microsoft Windows 98 開(kāi)發(fā)應用程序,你必須對某些問(wèn)題作出決定。本章也要講述Windows 98 的有關(guān)問(wèn)題。
2.1 字符集
軟件的本地化要解決的真正問(wèn)題,實(shí)際上就是如何來(lái)處理不同的字符集。多年來(lái),許多人一直將文本串作為一系列單字節字符來(lái)進(jìn)行編碼,并在結尾處放上一個(gè)零。對于我們來(lái)說(shuō),這已經(jīng)成了習慣。當調用s t r l e n 函數時(shí),它在以0 結尾的單字節字符數組中返回字符的數目。
問(wèn)題是,有些文字和書(shū)寫(xiě)規則(比如日文中的漢字就是個(gè)典型的例子)的字符集中的符號太多了,因此單字節(它提供的符號最多不能超過(guò)2 5 6 個(gè))是根本不敷使用的。為此出現了雙字節字符集(D B C S ),以支持這些文字和書(shū)寫(xiě)規則。
2.1.1 單字節與雙字節字符集
在雙字節字符集中,字符串中的每個(gè)字符可以包含一個(gè)字節或包含兩個(gè)字節。例如,日文中的漢字,如果第一個(gè)字符在0 x 8 1 與0 x 9 F 之間,或者在0 x E 0 與0 x F C 之間,那么就必須觀(guān)察下一個(gè)字節,才能確定字符串中的這個(gè)完整的字符。使用雙字節字符集,對于程序員來(lái)說(shuō)簡(jiǎn)直是個(gè)很大的難題,因為有些字符只有一個(gè)字節寬,而有些字符則是兩個(gè)字節寬。
如果只是調用s t r l e n 函數,那么你無(wú)法真正了解字符串中究竟有多少字符,它只能告訴你到達結尾的0 之前有多少個(gè)字節。A N S I 的C 運行期庫中沒(méi)有配備相應的函數,使你能夠對雙字節字符集進(jìn)行操作。但是,Microsoft Visual C++的運行期庫卻包含許多函數,如_ m b s l e n ,它可以用來(lái)操作多字節(既包括單字節也包括雙字節)字符串。
為了幫助你對D B C S 字符串進(jìn)行操作,Wi n d o w s 提供了下面的一組幫助函數(見(jiàn)表2 - 1 )。前兩個(gè)函數CharNext 和Char Prev 允許前向或逆向遍歷DBCS 字符串,方法是每次一個(gè)字符。第三個(gè)函數IsDBCSLeadByte, 在字節返回到一個(gè)兩字字節符的第一個(gè)字節時(shí)將返回T R U E 。
表2-1 對D B C S 字符串進(jìn)行操作的幫助函數
函數 描述
PTSTR CharNext(PCTSTR pszCurrentChar); 返回字符串中的下一個(gè)字符的地址
PTSTR CharPrev (PCTSTR pszStart,PCTSTR p s z C u r r e n t C h a r); 返回字符串中的上一個(gè)字符的地址
BOOL IsDBCSLeadByteTRUE(BYTE bTestChar); 如果該字節是DBCS 字符的第一個(gè)字節,則返回
盡管這些函數使得我們對D B C S 的操作更容易,但還需要,一個(gè)更好的方法讓我們來(lái)看看U n i c o d e 。
2.1.2 Unicode :寬字節字符集
U n i c o d e 是A p p l e 和X e r o x 公司于1 9 8 8 年建立的一個(gè)技術(shù)標準。1 9 9 1 年,成立了一個(gè)集團機構負責U n i c o d e 的開(kāi)發(fā)和推廣應用。該集團由A p p l e 、C o m p a q 、H P 、I B M 、M i c r o s o f t 、O r a c l e 、Silicon Graphics, Inc.、S y b a s e 、U n i s y s 和X e r o x 等公司組成(若要了解該集團的全部成員,請通過(guò)網(wǎng)址w w w. U n i c o d e . o rg 查找)。該集團負責維護U n i c o d e 標準。U n i c o d e 的完整描述可以參閱A d d i s o n We s l e y 出版的《Unicode Standard 》一書(shū)(該書(shū)可以通過(guò)網(wǎng)址w w w. U n i c o d e . o rg 訂購)。
U n i c o d e 提供了一種簡(jiǎn)單而又一致的表示字符串的方法。U n i c o d e 字符串中的所有字符都是1 6 位的(兩個(gè)字節)。它沒(méi)有專(zhuān)門(mén)的字節來(lái)指明下一個(gè)字節是屬于同一個(gè)字符的組成部分,還是一個(gè)新字符。這意味著(zhù)你只需要對指針進(jìn)行遞增或遞減,就可以遍歷字符串中的各個(gè)字符,不再需要調用C h a r N e x t 、C h a r P r e v 和I s D B C S L e a d B y t e 之類(lèi)的函數。
由于U n i c o d e 用一個(gè)1 6 位的值來(lái)表示每個(gè)字符,因此總共可以得到65 000 個(gè)字符,這樣,它就能夠對世界各國的書(shū)面文字中的所有字符進(jìn)行編碼,遠遠超過(guò)了單字節字符集的2 5 6 個(gè)字符的數目。
目前,已經(jīng)為阿拉伯文、中文拼音、西里爾字母(俄文)、希臘文、西伯萊文、日文、韓文和拉丁文(英文)字母定義了U n i c o d e 代碼點(diǎn)。(代碼點(diǎn)是字符集中符號的位置。)這些字符集中還包含了大量的標點(diǎn)符號、數學(xué)符號、技術(shù)符號、箭頭、裝飾標志、區分標志和其他許多字符。如果將所有這些字母和符號加在一起,總計約達3 5 0 0 0 個(gè)不同的代碼點(diǎn),這樣,總計65 000 多個(gè)代碼點(diǎn)中,大約還有一半可供將來(lái)擴充時(shí)使用。
這65536個(gè)字符可以分成不同的區域。表2-2 顯示了這樣的區域的一部分以及分配給這些區域的字符。
表2-2 區域字符
1 6 位代碼 字符 16 位代碼 字符
0 0 0 0 - 0 0 7 F A S C I I 0 3 0 0 - 0 3 6 F 通用區分標志
0 0 8 0 - 0 0 F F 拉丁文1 字符 0 4 0 0 - 0 4 F F 西里爾字母
0 1 0 0 - 0 1 7 F 歐洲拉丁文 0 5 3 0 - 0 5 8 F 亞美尼亞文
0 1 8 0 - 0 1 F F 擴充拉丁文 0 5 9 0 - 0 5 F F 西伯萊文
0 2 5 0 - 0 2 A F 標準拼音 0 6 0 0 - 0 6 F F 阿拉伯文
0 2 B 0 - 0 2 F F 修改型字母 0 9 0 0 - 0 9 7 F 梵文
目前尚未分配的代碼點(diǎn)大約還有29 000 個(gè),不過(guò)它們是保留供將來(lái)使用的。另外,大約有6 0 0 0 個(gè)代碼點(diǎn)是保留供個(gè)人使用的。
2.2 為什么使用Unicode
當開(kāi)發(fā)應用程序時(shí),當然應該考慮利用U n i c o d e 的優(yōu)點(diǎn)。即使現在你不打算對應用程序進(jìn)行本地化,開(kāi)發(fā)時(shí)將U n i c o d e 放在心上,肯定可以簡(jiǎn)化將來(lái)的代碼轉換工作。此外,U n i c o d e 還具備下列功能:
• 可以很容易地在不同語(yǔ)言之間進(jìn)行數據交換。
• 使你能夠分配支持所有語(yǔ)言的單個(gè)二進(jìn)制. e x e 文件或D L L 文件。
• 提高應用程序的運行效率(本章后面還要詳細介紹)。
2.3 Windows 2000與Unicode
Windows 2000 是使用U n i c o d e 從頭進(jìn)行開(kāi)發(fā)的,用于創(chuàng )建窗口、顯示文本、進(jìn)行字符串操作等的所有核心函數都需要U n i c o d e 字符串。如果調用任何一個(gè)Wi n d o w s 函數并給它傳遞一個(gè)A N S I 字符串,那么系統首先要將字符串轉換成U n i c o d e ,然后將U n i c o d e 字符串傳遞給操作系統。如果希望函數返回A N S I 字符串,系統就會(huì )首先將U n i c o d e 字符串轉換成A N S I 字符串,然后將結果返回給你的應用程序。所有這些轉換操作都是在你看不見(jiàn)的情況下發(fā)生的。當然,進(jìn)行這些字符串的轉換需要占用系統的時(shí)間和內存。
例如,如果調用C r e a t e Wi n d o w E x 函數,并傳遞類(lèi)名字和窗口標題文本的非U n i c o d e 字符串,那么C r e a t e Wi n d o w E x 必須分配內存塊(在你的進(jìn)程的默認堆中),將非U n i c o d e 字符串轉換成U n i c o d e 字符串,并將結果存儲在分配到的內存塊中,然后調用U n i c o d e 版本的C r e a t e Wi n d o w E x函數。
對于用字符串填入緩存的函數來(lái)說(shuō),系統必須首先將U n i c o d e 字符串轉換成非U n i c o d e 字符串,然后你的應用程序才能處理該字符串。由于系統必須執行所有這些轉換操作,因此你的應用程序需要更多的內存,并且運行的速度比較慢。通過(guò)從頭開(kāi)始用U n i c o d e 來(lái)開(kāi)發(fā)應用程序,就能夠使你的應用程序更加有效地運行。
2.4 Windows 98與Unicode
Windows 98 不是一種全新的操作系統。它繼承了1 6 位Wi n d o w s 操作系統的特性,它不是用來(lái)處理U n i c o d e 的。如果要增加對U n i c o d e 的支持,其工作量非常大,因此在該產(chǎn)品的特性列表中沒(méi)有包括這個(gè)支持項目。由于這個(gè)原因,Windows 98 像它的前任產(chǎn)品一樣,幾乎都是使用A N S I 字符串來(lái)進(jìn)行所有的內部操作的。
仍然可以編寫(xiě)用于處理U n i c o d e 字符和字符串的Wi n d o w s 應用程序,不過(guò),使用Wi n d o w s 函數要難得多。例如,如果想要調用C r e a t e Wi n d o w E x 函數并將A N S I 字符串傳遞給它,這個(gè)調用的速度非???,不需要從你進(jìn)程的默認堆棧中分配緩存,也不需要進(jìn)行字符串轉換。但是,如果想要調用C r e a t e Wi n d o w E x 函數并將U n i c o d e 字符串傳遞給它,就必須明確分配緩存,并調用函數,以便執行從U n i c o d e 到A N S I 字符串的轉換操作。然后可以調用C r e a t e Wi n d o w E x ,傳遞A N S I 字符串。當C r e a t e Wi n d o w E x 函數返回時(shí),就能釋放臨時(shí)緩存。這比使用Windows 2000 上的U n i c o d e 要麻煩得多。本章的后面要介紹如何在Windows 98 下進(jìn)行這些轉換。
雖然大多數U n i c o d e 函數在Windows 98 中不起任何作用,但是仍有少數U n i c o d e 函數確實(shí)非常有用。這些函數是:
■E n u m R e s o u r c e L a n g u a g e s W ■G e t Te x t E x t e n t P o i n t 3 2 W
■E n u m R e s o u r c e N a m e s W ■G e t Te x t E x t e n t P o i n t W
■E n u m R e s o u r c e Ty p e s W ■L s t r l e n W
■E x t Te x t O u t W ■M e s s a g e B o x E xW
■F i n d R e s o u r c e W ■M e s s a g e B o x W
■F i n d R e s o u r c e E x W ■Te x t O u t W
■G e t C h a r Wi d t h W ■Wi d e C h a r To M u l t i B y t e
■G e t C o m m a n d L i n e W ■M u l t iBy t e To Wi d e C h a r
可惜的是,這些函數中有許多函數在Windows 98 中會(huì )出現各種各樣的錯誤。有些函數無(wú)法使用某些字體,有些函數會(huì )破壞內存堆棧,有些函數會(huì )使打印機驅動(dòng)程序崩潰,等等。如果要使用這些函數,必須對它們進(jìn)行大量的測試。即使這樣,可能仍然無(wú)法解決問(wèn)題。因此必須向用戶(hù)說(shuō)明這些情況。
2.5 Windows CE與Unicode
Windows CE 操作系統是為小型設備開(kāi)發(fā)的,這些設備的內存很小,并且不帶磁盤(pán)存儲器。你可能認為,由于M i c r o s o f t 公司的主要目標是建立一種盡可能小的操作系統,因此它會(huì )使用A N S I 作為自己的字符集。但是M i c r o s o f t 公司并非鼠目寸光,他們懂得,Windows CE 的設備要在世界各地銷(xiāo)售,他們希望降低軟件開(kāi)發(fā)成本,這樣就能更加容易地開(kāi)發(fā)應用程序。為此,Windows CE 本身就是使用U n i c o d e 的一種操作系統。
但是,為了使Windows CE 盡量做得小一些,M i c r o s o f t 公司決定完全不支持ANSI Wi n d o w s函數。因此,如果要為Windows CE 開(kāi)發(fā)應用程序,必須懂得U n i c o d e ,并且在整個(gè)應用程序中使用U n i c o d e 。
2.6 需要注意的問(wèn)題
下面讓我們進(jìn)一步明確一下“M i c r o s o f t 公司對U n i c o d e 支持的情況”:
• Windows 2000 既支持U n i c o d e ,也支持A N S I ,因此可以為任意一種開(kāi)發(fā)應用程序。
• Windows 98 只支持A N S I ,只能為A N S I 開(kāi)發(fā)應用程序。
• Windows CE 只支持U n i c o d e ,只能為U n i c o d e 開(kāi)發(fā)應用程序。
雖然M i c r o s o f t 公司試圖讓軟件開(kāi)發(fā)人員能夠非常容易地開(kāi)發(fā)在這3 種平臺上運行的軟件,但是U n i c o d e 與A N S I 之間的差異使得事情變得困難起來(lái),并且這種差異通常是我遇到的最大的問(wèn)題之一。請不要誤解,M i c r o s o f t 公司堅定地支持U n i c o d e ,并且我也堅決鼓勵你使用它。不過(guò)你應該懂得,你可能遇到一些問(wèn)題,需要一定的時(shí)間來(lái)解決這些問(wèn)題。建議你盡可能使用U n i c o d e 。如果運行Windows 98 ,那么只有在必要時(shí)才需轉換到A N S I 。不過(guò),還有另一個(gè)小問(wèn)題你應該了解,那就是C O M 。
2.7 對COM的簡(jiǎn)單說(shuō)明
當M i c r o s o f t 公司將C O M 從1 6 位Wi n d o w s 轉換成Wi n 3 2 時(shí),公司作出了一個(gè)決定,即需要字符串的所有C O M 接口方法都只能接受U n i c o d e 字符串。這是個(gè)了不起的決定,因為C O M 通常用于使不同的組件能夠互相進(jìn)行通信,而U n i c o d e 則是傳遞字符串的最佳手段。
如果你為Windows 2000 或Windows CE 開(kāi)發(fā)應用程序,并且也使用C O M ,那么你將會(huì )如虎添翼。在你的整個(gè)源代碼中使用U n i c o d e ,將使與操作系統進(jìn)行通信和與C O M 對象進(jìn)行通信的操作變成一件輕而易舉的事情。
如果你為Windows 98 開(kāi)發(fā)應用程序,并且也使用C O M ,那么將會(huì )遇到一些問(wèn)題。C O M 要求使用U n i c o d e 字符串,而操作系統的大多數函數要求使用A N S I 字符串。那是多么難辦的事情??!我曾經(jīng)從事過(guò)若干個(gè)項目的開(kāi)發(fā),在這些項目中,我編寫(xiě)了許多代碼,僅僅是為了來(lái)回進(jìn)行字符串的轉換。
2.8 如何編寫(xiě)Unicode源代碼
M i c r o s o f t 公司為U n i c o d e 設計了Windows API ,這樣,可以盡量減少對你的代碼的影響。實(shí)際上,你可以編寫(xiě)單個(gè)源代碼文件,以便使用或者不使用U n i c o d e 來(lái)對它進(jìn)行編譯。只需要定義兩個(gè)宏(U N I C O D E 和_ U N I C O D E ),就可以修改然后重新編譯該源文件。
2.8.1 C 運行期庫對Unicode的支持
為了利用U n i c o d e 字符串,定義了一些數據類(lèi)型。標準的C 頭文件S t r i n g . h 已經(jīng)作了修改,以便定義一個(gè)名字為w c h a r _ t 的數據類(lèi)型,它是一個(gè)U n i c o d e 字符的數據類(lèi)型:
typedef unsigned short wchar_t;
例如,如果想要創(chuàng )建一個(gè)緩存,用于存放最多為9 9 個(gè)字符的U n i c o d e 字符串和一個(gè)結尾為零的字符,可以使用下面這個(gè)語(yǔ)句:
wchar_t szBuffer[100];
該語(yǔ)句創(chuàng )建了一個(gè)由1 0 0 個(gè)1 6 位值組成的數組。當然,標準的C 運行期字符串函數,如s t r c p y 、s t r c h r 和s t r c a t 等,只能對A N S I 字符串進(jìn)行操作,不能正確地處理U n i c o d e 字符串。因此,ANSI C 也擁有一組補充函數。清單2 - 1 顯示了一些標準的ANSI C 字符串函數,后面是它們的等價(jià)U n i c o d e 函數。
char * strcat(char *,const char *);
wchar_t * wcscat(wchar_t *,const wchar_t *);
清單2-1 標準的ANSI C 字符串函數和它們的等價(jià)U n i c o d e 函數
char * strchr(const char *,int);
wchar_t * wcschr(const wchar_t *,wchar_t);
int strcmp(const char *,const char *);
int wcscmp(const wchar_t *,const wchar_t *);
char * strcpy(char *,const char *);
wchar_t * wcscpy(wchar_t *,const wchar_t *);
size_t strlen(const char *);
size_t wcslen(const wchar_t *);
請注意,所有的U n i c o d e 函數均以w c s 開(kāi)頭,w c s 是寬字符串的英文縮寫(xiě)。若要調用U n i c o d e函數,只需用前綴w c s 來(lái)取代A N S I 字符串函數的前綴s t r 即可。
注意大多數軟件開(kāi)發(fā)人員可能已經(jīng)不記得這樣一個(gè)非常重要的問(wèn)題了,那就是M i c r o s o f t 公司提供的C 運行期庫與A N S I 的標準C 運行期庫是一致的。ANSI C 規定,C運行期庫支持U n i c o d e 字符和字符串。這意味著(zhù)始終都可以調用C 運行期函數,以便對U n i c o d e 字符和字符串進(jìn)行操作,即使是在Windows 98 上運行,也可以調用這些函數。換句話(huà)說(shuō),w c s c a t 、w c s l e n 和w c s t o k 等函數都能夠在Windows 98 上很好地運行,這些都是必須關(guān)心的操作系統函數。
對于包含了對s t r 函數或w c s 函數進(jìn)行顯式調用的代碼來(lái)說(shuō),無(wú)法非常容易地同時(shí)為A N S I 和U n i c o d e 對這些代碼進(jìn)行編譯。本章前面說(shuō)過(guò),可以創(chuàng )建同時(shí)為A N S I 和U n i c o d e 進(jìn)行編譯的單個(gè)源代碼文件。若要建立雙重功能,必須包含T C h a r. h 文件,而不是包含S t r i n g . h 文件。
T C h a r. h 文件的唯一作用是幫助創(chuàng )建A N S I / U n i c o d e 通用源代碼文件。它包含你應該用在源代碼中的一組宏,而不應該直接調用s t r 函數或者w c s 函數。如果在編譯源代碼文件時(shí)定義了U N I C O D E ,這些宏就會(huì )引用w c s 這組函數。如果沒(méi)有定義_ U N I C O D E ,那么這些宏將引用s t r這組宏。
例如,在T C h a r. h 中有一個(gè)宏稱(chēng)為_(kāi) t c s c p y 。如果在包含該頭文件時(shí)沒(méi)有定義_ U N I C O D E ,那么_ t c s c p y 就會(huì )擴展為A N S I 的s t r c p y 函數。但是如果定義了_UNICODE, _tcscpy 將擴展為U n i c o d e的w c s c p y 函數。擁有字符串參數的所有C 運行期函數都在T C h a r. h 文件中定義了一個(gè)通用宏。如果使用通用宏,而不是A N S I / U n i c o d e 的特定函數名,就能夠順利地創(chuàng )建可以為A N S I 或U n i c o d e進(jìn)行編譯的源代碼。
但是,除了使用這些宏之外,還有一些操作是必須進(jìn)行的。T C h a r. h 文件包含了另外一些宏.若要定義一個(gè)A N S I / U n i c o d e 通用的字符串數組,請使用下面的T C H A R 數據類(lèi)型。如果定義了_ U N I C O D E ,T C H A R 將聲明為下面的形式:
typedef wchar_t TCHAR;
如果沒(méi)有定義_ U N I C O D E ,則T C H A R 將聲明為下面的形式:
typedef char TCHAR;
使用該數據類(lèi)型,可以像下面這樣分配一個(gè)字符串:
TCHAR szString[100];
也可以創(chuàng )建對字符串的指針:
TCHAR *szError="Error";
不過(guò)上面這行代碼存在一個(gè)問(wèn)題。按照默認設置,M i c r o s o f t 公司的C + +編譯器能夠編譯所有的字符串,就像它們是A N S I 字符串,而不是U n i c o d e 字符串。因此,如果沒(méi)有定義_ U N I C O D E ,該編譯器將能正確地編譯這一行代碼。但是,如果定義了_ U N I C O D E ,就會(huì )產(chǎn)生一個(gè)錯誤。若要生成一個(gè)U n i c o d e 字符串而不是A N S I 字符串,必須將該代碼行改寫(xiě)為下面的樣子:
TCHAR *szError=L"Error";
字符串(literal string )前面的大寫(xiě)字母L ,用于告訴編譯器該字符串應該作為U n i c o d e 字符串來(lái)編譯。當編譯器將字符串置于程序的數據部分中時(shí),它在每個(gè)字符之間分散插入零字節。這種變更帶來(lái)的問(wèn)題是,現在只有當定義了_ U N I C O D E 時(shí),程序才能成功地進(jìn)行編譯。我們需要另一個(gè)宏,以便有選擇地在字符串的前面加上大寫(xiě)字母L 。這項工作由_ T E X T 宏來(lái)完成,_ T E X T 宏也在T C h a r. h 文件中做了定義。如果定義了_ U N I C O D E ,那么_ T E X T 定義為下面的形式:
#define _TEXT(x) L ## x
如果沒(méi)有定義_ U N I C O D E ,_ T E X T 將定義為
#define _TEXT(x) x
使用該宏,可以改寫(xiě)上面這行代碼,這樣,無(wú)論是否定義了_ U N I C O D E 宏,它都能夠正確地進(jìn)行編譯。如下所示:
TCHAR *szError=_TEXT("Error");
_ T E X T 宏也可以用于字符串。例如,若要檢查一個(gè)字符串的第一個(gè)字符是否是大寫(xiě)字母J ,只需編寫(xiě)下面的代碼即可:
if(szError[0]==_TEXT('J')){
//First character is a 'J'
...
}
else{
//First character is not a 'J'
...
}
2.8.2 Windows定義的Unicode數據類(lèi)型
Wi n d o w s 頭文件定義了表2 - 3 列出的數據類(lèi)型。
表2-3 Uincode 數據類(lèi)型
數據類(lèi)型 說(shuō)明
W C H A R U n i c o d e 字符
P W S T R 指向U n i c o d e 字符串的指針
P C W S T R 指向一個(gè)恒定的U n i c o d e 字符串的指針
這些數據類(lèi)型是指U n i c o d e 字符和字符串。Wi n d o w s 頭文件也定義了A N S I / U n i c o d e 通用數據類(lèi)型P T S T R 和P C T S T R 。這些數據類(lèi)型既可以指A N S I 字符串,也可以指U n i c o d e 字符串,這取決于當編譯程序模塊時(shí)是否定義了U N I C O D E 宏。
請注意,這里的U N I C O D E 宏沒(méi)有前置的下劃線(xiàn)。_ U N I C O D E 宏用于C 運行期頭文件,而U N I C O D E 宏則用于Wi n d o w s 頭文件。當編譯源代碼模塊時(shí),通常必須同時(shí)定義這兩個(gè)宏。
2.8.3 Windows中的Unicode函數和ANSI函數
前面已經(jīng)講過(guò),有兩個(gè)函數稱(chēng)為C r e a t e Wi n d o w E x ,一個(gè)C r e a t e Wi n d o w E x 接受U n i c o d e 字符串,另一個(gè)C r e a t e Wi n d o w E x 接受A N S I 字符串。情況確實(shí)如此,不過(guò),這兩個(gè)函數的原型實(shí)際上是下面的樣子:
HWND WINAPI CreateWindowExW(
DWORD dwExStyle, // extended window style
LPCTSTR lpClassName, // pointer to registered class name
LPCTSTR lpWindowName, // pointer to window name
DWORD dwStyle, // window style
int x, // horizontal position of window
int y, // vertical position of window
int nWidth, // window width
int nHeight, // window height
HWND hWndParent, // handle to parent or owner window
HMENU hMenu, // handle to menu, or child-window identifier
HINSTANCE hInstance, // handle to application instance
LPVOID lpParam // pointer to window-creation data
);
HWND WINAPI CreateWindowExA(
DWORD dwExStyle, // extended window style
PCTSTR pClassName, // pointer to registered class name
PCTSTR pWindowName, // pointer to window name
DWORD dwStyle, // window style
int x, // horizontal position of window
int y, // vertical position of window
int nWidth, // window width
int nHeight, // window height
HWND hWndParent, // handle to parent or owner window
HMENU hMenu, // handle to menu, or child-window identifier
HINSTANCE hInstance, // handle to application instance
PVOID pParam // pointer to window-creation data
);
C r e a t e Wi n d o w E x W 是接受U n i c o d e 字符串的函數版本。函數名結尾處的大寫(xiě)字母W 是英文w i d e(寬)的縮寫(xiě)。每個(gè)U n i c o d e 字符的長(cháng)度是1 6 位,因此,它們常常稱(chēng)為寬字符。C r e a t e Wi n d o w E x A的結尾處的大寫(xiě)字母A 表示該函數可以接受A N S I 字符串。
但是,在我們的代碼中,通常只包含了對C r e a t e Wi n d o w E x 的調用,而不是直接調用C r e a t e Wi n d o w E x W 或者C r e a t e Wi n d o w E x A 。在Wi n U s e r. h 文件中,C r e a t e Wi n d o w E x 實(shí)際上是定義為下面這種形式的一個(gè)宏:
#ifdef UNICODE
#define CreateWindowEx CreateWindowExW
#else
#define CreateWindowEx CreateWindowExA
#endif //!UNICODE
當編譯源代碼模塊時(shí),U N I C O D E 是否已經(jīng)作了定義,將決定你調用的是哪個(gè)C r e a t e Wi n d o w E x 版本。當轉用一個(gè)1 6 位的Wi n d o w s 應用程序時(shí),你在編譯期間可能沒(méi)有定義U N I C O D E 。對C r e a t e Wi n d o w E x 函數的任何調用都會(huì )將該宏擴展為對C r e a t e Wi n d o w E x A 的調用,即對C r e a t e Wi n d o w E x 的A N S I 版本的調用。由于1 6 位Wi n d o w s 只提供了C r e a t e Wi n d o w s E x 的A N S I 版本,因此可以比較容易地轉用它的應用程序。
在Windows 2000 下,M i c r o s o f t 的C r e a t e Wi n d o w E x A 源代碼只不過(guò)是一個(gè)形實(shí)替換程序層或翻譯層,用于分配內存,以便將A N S I 字符串轉換成U n i c o d e 字符串。該代碼然后調用C r e a t eWi n d o w E x W ,并傳遞轉換后的字符串。當C r e a t e Wi n d o w E x W 返回時(shí),C r e a t e Wi n d o w E x A 便釋放它的內存緩存,并將窗口句柄返回給你。
如果要創(chuàng )建其他軟件開(kāi)發(fā)人員將要使用的動(dòng)態(tài)鏈接庫(D L L ),請考慮使用下面的方法。在D L L 中提供兩個(gè)輸出函數。一個(gè)是A N S I 版本,另一個(gè)是U n i c o d e 版本。在A(yíng) N S I 版本中,只需要分配內存,執行必要的字符串轉換,并調用該函數的U n i c o d e 版本(本章后面部分介紹這個(gè)進(jìn)程)。
在Windows 98 下,M i c r o s o f t 的C r e a t e Wi n d o w E x A 源代碼是執行操作的函數。Windows 98提供了接受U n i c o d e 參數的所有Wi n d o w s 函數的進(jìn)入點(diǎn),但是這些函數并不將U n i c o d e 字符串轉換成A N S I 字符串,它們只返回運行失敗的消息。調用G e t L a s t E r r o r 將返回E R R O R _C A L L _ N O T _ I M P L E M E N T E D 。這些函數中只有A N S I 版本的函數才能正確地運行。如果編譯的代碼調用了任何寬字符函數,應用程序將無(wú)法在Windows 98 下運行。
Windows API 中的某些函數,比如Wi n E x e c 和O p e n F i l e 等,只是為了實(shí)現與1 6 位Wi n d o w s 程序的向后兼容而存在,因此,應該避免使用。應該使用對C r e a t e P r o c e s s 和C r e a t e F i l e 函數的調用來(lái)取代對Wi n E x e c 和O p e n F i l e 函數的調用。從系統內部來(lái)講,老的函數完全可以調用新的函數。老的函數存在的一個(gè)大問(wèn)題是,它們不接受U n i c o d e 字符串。當調用這些函數時(shí),必須傳遞A N S I 字符串。另一方面,所有新的和未過(guò)時(shí)的函數在Windows 2000 中都同時(shí)擁有A N S I 和U n i c o d e 兩個(gè)版本。
2.8.4 Windows字符串函數
Wi n d o w s 還提供了一組范圍很廣的字符串操作函數。這些函數與C 運行期字符串函數(如s t r c p y 和w c s c p y )很相似。但是該操作系統函數是操作系統的一個(gè)組成部分,操作系統的許多組件都使用這些函數,而不使用C 運行期庫。建議最好使用操作系統函數,而不要使用C 運行期字符串函數。這將有助于稍稍提高你的應用程序的運行性能,因為操作系統字符串函數常常被大型應用程序比如操作系統的外殼進(jìn)程E x p l o r e r. e x e 所使用。由于這些函數使用得很多,因此,在你的應用程序運行時(shí),它們可能已經(jīng)被裝入R A M 。
若要使用這些函數,系統必須運行Windows 2000 或Windows 98 。如果安裝了I n t e r n e t Explorer 4.0 或更新的版本,也可以在較早的Wi n d o w s 版本中獲得這些函數。在經(jīng)典的操作系統函數樣式中,操作系統字符串函數名既包含大寫(xiě)字母,也包含小寫(xiě)字母,它的形式類(lèi)似這個(gè)樣子:S t r C a t 、S t r C h r 、S t r C m p 和S t r C p y 等。若要使用這些函數,必須加上S h l WA p i . h 頭文件。另外,如前所述,這些字符串函數既有A N S I 版本,也有U n i c o d e 版本,例如S t r C a t A 和S t r C a t W 。由于這些函數屬于操作系統函數,因此,當創(chuàng )建應用程序時(shí),如果定義了U N I C O D E (不帶前置下劃線(xiàn)),那么它們的符號將擴展為寬字符版本。
2.9 成為符合ANSI和Unicode的應用程序
即使你不打算立即使用U n i c o d e ,最好也應該著(zhù)手將你的應用程序轉換成符合U n i c o d e 的應用程序。下面是應該遵循的一些基本原則:
• 將文本串視為字符數組,而不是c h a r s 數組或字節數組。
• 將通用數據類(lèi)型(如T C H A R 和P T S T R )用于文本字符和字符串。
• 將顯式數據類(lèi)型(如B Y T E 和P B Y T E )用于字節、字節指針和數據緩存。
• 將T E X T 宏用于原義字符和字符串。
• 執行全局性替換(例如用P T S T R 替換P S T R )。
• 修改字符串運算問(wèn)題。例如函數通常希望你在字符中傳遞一個(gè)緩存的大小,而不是字節。
這意味著(zhù)你不應該傳遞s i z e o f ( s z B u ff e r ) ,而應該傳遞(s i z e o f ( s z B u ff e r ) / s i z e o f ( T C H A R )。另外,如果需要為字符串分配一個(gè)內存塊,并且擁有該字符串中的字符數目,那么請記住要按字節來(lái)分配內存。這就是說(shuō),應該調用malloc(nCharacters *sizeof(TCHAR)),而不是調用m a l l o c( n C h a r a c t e r s )。在上面所說(shuō)的所有原則中,這是最難記住的一條原則,如果操作錯誤,編譯器將不發(fā)出任何警告。
當我為本書(shū)的第一版編寫(xiě)示例程序時(shí),我編寫(xiě)的原始程序只能編譯為A N S I 程序。后來(lái),當我開(kāi)始撰寫(xiě)本章的內容時(shí),我想我應該鼓勵使用U n i c o d e ,并且打算創(chuàng )建一些示例程序,以便展示你可以非常容易地編寫(xiě)既可以用U n i c o d e 也可以用A N S I 來(lái)編譯的程序。這時(shí)我發(fā)現最好的辦法是將本書(shū)的所有示例程序進(jìn)行轉換,使它們都能夠用U n i c o d e 和A N S I 進(jìn)行編譯。
我用了大約4 個(gè)小時(shí)將所有程序進(jìn)行了轉換??紤]到我以前從來(lái)沒(méi)有這方面的轉換經(jīng)驗,這個(gè)速度是相當不錯了。
2.9.1 Windows字符串函數
Wi n d o w s 也提供了一組用于對U n i c o d e 字符串進(jìn)行操作的函數,表2 - 4 對它們進(jìn)行了描述。
表2-4 對U n i c o d e 字符串進(jìn)行操作的函數
函數 描述
l s t r c a t 將一個(gè)字符串置于另一個(gè)字符串的結尾處
l s t r c m p 對兩個(gè)字符串進(jìn)行區分大小寫(xiě)的比較
l s t r c m p i 對兩個(gè)字符串進(jìn)行不區分大小寫(xiě)的比較
l s t r c p y 將一個(gè)字符串拷貝到內存中的另一個(gè)位置
l s t r l e n 返回字符串的長(cháng)度(按字符數來(lái)計量)
這些函數是作為宏來(lái)實(shí)現的,這些宏既可以調用函數的U n i c o d e 版本,也可以調用函數的A N S I 版本,這要根據編譯源代碼模塊時(shí)是否已經(jīng)定義了U N I C O D E 而定。例如,如果沒(méi)有定義U N I C O D E ,l s t r c a t 函數將擴展為l s t r c a t A 。如果定義了U N I C O D E ,l s t r c a t 將擴展為l s t r c a t W 。
有兩個(gè)字符串函數,即l s t r c m p 和l s t r c m p i ,它們的行為特性與等價(jià)的C 運行期函數是不同的。C 運行期函數s t r c m p 、s t r c m p i 、w c s c m p 和w c s c m p i 只是對字符串中的代碼點(diǎn)的值進(jìn)行比較,這就是說(shuō),這些函數將忽略實(shí)際字符的含義,只是將第一個(gè)字符串中的每個(gè)字符的數值與第二個(gè)字符串中的字符的數值進(jìn)行比較。而Wi n d o w s 函數l s t r c m p 和l s t r c m p i 是作為對Wi n d o w s 函數C o m p a r e S t r i n g 的調用來(lái)實(shí)現的。
int CompareString(
LCID lcid,
DWORD fdwStyle,
PCWSTR pString1,
int cch1,
PCTSTR pString2,
int cch2);
該函數對兩個(gè)U n i c o d e 字符串進(jìn)行比較。C o m p a r e S t r i n g 的第一個(gè)參數用于設定語(yǔ)言I D(L C I D ),這是個(gè)3 2 位值,用于標識一種特定的語(yǔ)言。C o m p a r e S t r i n g 使用這個(gè)L C I D 來(lái)比較這兩個(gè)字符串,方法是對照一種特定的語(yǔ)言來(lái)查看它們的字符的含義。這種操作方法比C 運行期函數簡(jiǎn)單地進(jìn)行數值比較更有意義。
當l s t r c m p 函數系列中的任何一個(gè)函數調用C o m p a r e S t r i n g 時(shí),該函數便將調用Wi n d o w s 的G e t T h r e a d S t r i n g 函數的結果作為第一個(gè)參數來(lái)傳遞:
LCID GetThreadLocale();
每次創(chuàng )建一個(gè)線(xiàn)程時(shí),它就被賦予一種語(yǔ)言。函數將返回該線(xiàn)程的當前語(yǔ)言設置。
C o m p a r e S t r i n g 的第二個(gè)參數用于標識一些標志,這些標志用來(lái)修改該函數比較兩個(gè)字符串時(shí)所用的方法。表2 - 5 顯示了可以使用的標志。
表2-5 CompareString 的標志及含義
標志 含義
N O R M _ I G N O R E C A S E 忽略字母的大小寫(xiě)
N O R M _ I G N O R E K A N AT Y P E 不區分平假名與片假名字符
N O R M _ I G N O R E N O N S PA C E 忽略無(wú)間隔字符
N O R M _ I G N O R E S Y M B O L S 忽略符號
N O R M _ I G N O R E W I D T H 不區分單字節字符與作為雙字節字符的同一個(gè)字符
S O RT _ S T R I N G S O RT 將標點(diǎn)符號作為普通符號來(lái)處理
當l s t r c m p 調用C o m p a r e S t r i n g 時(shí),它傳遞0 作為f d w S t y l e 的參數。但是,當l s t r c m p i 調用C o m p a r e S t r i n g 時(shí),它就傳遞N O R M _ I G N O R E C A S E 。C o m p a r e S t r i n g 的其余4 個(gè)參數用于設定兩個(gè)字符串和它們各自的長(cháng)度。如果為c c h 1 參數傳遞- 1 ,那么該函數將認為p S t r i n g 1 字符串是以0結尾,并計算該字符串的長(cháng)度。對于p S t r i n g 2 字符串來(lái)說(shuō),參數c c h 2 的作用也是一樣。
其他C 運行期函數沒(méi)有為U n i c o d e 字符串的操作提供很好的支持。例如,t o l o w e r 和t o u p p e r函數無(wú)法正確地轉換帶有重音符號的字符。為了彌補C 運行期庫中的這些不足,必須調用下面這些Wi n d o w s 函數,以便轉換U n i c o d e 字符串的大小寫(xiě)字母。這些函數也可以正確地用于A(yíng) N S I字符串。
頭兩個(gè)函數:
PTSTR CharLower(PTSTR pszString);
PTSTR CharUpper(PTSTR pszString);
既可以轉換單個(gè)字符,也可以轉換以0 結尾的整個(gè)字符串。若要轉換整個(gè)字符串,只需要傳遞字符串的地址即可。若要轉換單個(gè)字符,必須像下面這樣傳遞各個(gè)字符:
將單個(gè)字符轉換成一個(gè)P T S T R ,便可調用該函數,將一個(gè)值傳遞給它,在這個(gè)值中,較低的1 6 位包含了該字符,較高的1 6 位包含0 。當該函數看到較高位是0 時(shí),該函數就知道你想要轉換單個(gè)字符,而不是整個(gè)字符串。返回的值是個(gè)3 2 位值,較低的1 6 位中是已經(jīng)轉換的字符。
TCHAR cLowerCase=CharLower((PTSTR szString[0]);
將單個(gè)字符轉換成一個(gè)P T S T R ,便可調用該函數,將一個(gè)值傳遞給它,在這個(gè)值中,較低的1 6 位包含了該字符,較高的1 6 位包含0 。當該函數看到較高位是0 時(shí),該函數就知道你想要轉換單個(gè)字符,而不是整個(gè)字符串。返回的值是個(gè)3 2 位值,較低的1 6 位中是已經(jīng)轉換的字符。
下面兩個(gè)函數與前面兩個(gè)函數很相似,差別在于它們用于轉換緩存中包含的字符(該緩存不必以0 結尾):
DWORD CharLowerBuff(
/* pointer to buffer containing characters to process */
PTSTR pszString,
/* number of bytes or characters to process */
DWORD cchLength
);
DWORD CharUpperBuff(
/* pointer to buffer containing characters to process */
LPTSTR lpsz,
/* number of characters to process */
DWORD cchLength
);
其他的C 運行期函數,如i s a l p h a 、i s l o w e r 和i s u p p e r ,返回一個(gè)值,指明某個(gè)字符是字母字符、小寫(xiě)字母還是大寫(xiě)字母。Windows API 提供了一些函數,也能返回這些信息,但是Wi n d o w s 函數也要考慮用戶(hù)在控制面板中指定的語(yǔ)言:
BOOL IsCharAlpha(TCHAR ch);
BOOL IsCharAlphaNumeric(TCHAR ch);
BOOL IsCharLower(TCHAR ch);
BOOL IsCharUpper(TCHAR ch);
p r i n t f 函數家族是要介紹的最后一組C 運行期函數。如果在定義了_ U N I C O D E 的情況下編譯你的源代碼模塊,那么p r i n t f 函數家族便希望所有字符和字符串參數代表U n i c o d e 字符和字符串。但是,如果在沒(méi)有定義_ U N I C O D E 的情況下編譯你的源代碼模塊,p r i n t f 函數家族便希望傳遞給它的所有字符和字符串都是A N S I 字符和字符串。
M i c r o s o f t 公司已經(jīng)給C 運行期的p r i n t f 函數家族增加了一些特殊的域類(lèi)型。其中有些域類(lèi)型尚未被ANSI C 采用。新類(lèi)型使你能夠很容易地對A N S I 和U n i c o d e 字符和字符串進(jìn)行混合和匹配。操作系統的w s p r i n t f 函數也得到了增強。下面是一些例子(請注意大寫(xiě)S 和小寫(xiě)s 的使用):
char szA[100]; //An ANSI string buffer
WCHAR szW[100]; //A Unicode string buffer
//Normal sprintf:all strings are ANSI
sprintf(szA, "%s","ANSI Str");
//Converts Unicode string to ANSI
sprintf(szA,"%S",L"Unicode Str");
//Normal swprintf:all strings are Unicode
swprintf(szW,L"%s",L"Unicode Str");
//Converts ANSI string to Unicode
swprintf(szW,L"%S", "ANSI Str");
2.9.2 資源
當資源編譯器對你的所有資源進(jìn)行編譯時(shí),輸出文件是資源的二進(jìn)制文件。資源(字符串表、對話(huà)框模板和菜單等)中的字符串值總是寫(xiě)作U n i c o d e 字符串。在Windows 98 和Wi n d o w s 2 0 0 0 下,如果應用程序沒(méi)有定義U N I C O D E 宏,那么系統就會(huì )進(jìn)行內部轉換。
例如,如果在編譯源代碼模塊時(shí)沒(méi)有定義U N I C O D E ,調用L o a d S t r i n g 實(shí)際上就是調用L o a d S t r i n g A 函數。這時(shí)L o a d S t r i n g A 就從你的資源中讀取字符串,并將該字符串轉換成A N S I 字符串。A N S I 形式的字符串將從該函數返回給你的應用程序。
2.9.3 確定文本是ANSI文本還是Unicode文本
到現在為止,U n i c o d e 文本文件仍然非常少。實(shí)際上,M i c r o s o f t 公司自己的大多數產(chǎn)品并沒(méi)有配備任何U n i c o d e 文本文件。但是預計將來(lái)這種情況是會(huì )改變的(盡管這需要一個(gè)很長(cháng)的過(guò)程)。當然,Windows 2000 的N o t e p a d (記事本)應用程序允許你既能打開(kāi)U n i c o d e 文件,也能打開(kāi)A N S I 文件,并且可以創(chuàng )建這些文件。圖2 - 1 顯示了N o t e p a d 的Save As (文件另存為)對話(huà)框。請注意可以用不同的方法來(lái)保存文本文件。
圖2-1 Windows 2000 Notepad 的File Save As 對話(huà)框
對于許多用來(lái)打開(kāi)文本文件和處理這些文件的應用程序(如編譯器)來(lái)說(shuō),打開(kāi)一個(gè)文件后,應用程序就能方便地確定該文本文件是包含A N S I 字符還是U n i c o d e 字符。I s Te x t U n i c o d e 函數能夠幫助進(jìn)行這種區分:
DWORD IsTextUnicode(CONST PVOID pvBuffer, int cb,PINT pResult);
文本文件存在的問(wèn)題是,它們的內容沒(méi)有嚴格和明確的規則,因此很難確定該文件是包含A N S I 字符還是U n i c o d e 字符。I s Te x t U n i c o d e 使用一系列統計方法和定性方法,以便猜測緩存的內容。由于這不是一種確切的科學(xué)方法,因此I s Te x t U n i c o d e 有可能返回不正確的結果。
第一個(gè)參數p v B u ff e r 用于標識要測試的緩存的地址。該數據是個(gè)無(wú)效指針,因為你不知道你擁有的是A N S I 字符數組還是U n i c o d e
字符數組。
第二個(gè)參數c b 用于設定p v B u ff e r 指向的字節數。同樣,由于你不知道緩存中放的是什么,因此c b 是個(gè)字節數,而不是字符數。請注意,不必設定緩存的整個(gè)長(cháng)度。當然,I s Te x t U n i c o d e能夠測試的字節越多,得到的結果越準確。
第三個(gè)參數p R e s u l t 是個(gè)整數的地址,必須在調用I s Te x t U n i c o d e 之前對它進(jìn)行初始化。對該整數進(jìn)行初始化后,就可以指明你要I s Te x t U n i c o d e 執行哪些測試。也可以為該參數傳遞N U L L ,在這種情況下,I s Te x t U n i c o d e 將執行它能夠進(jìn)行的所有測試(詳細說(shuō)明請參見(jiàn)Platform SDK 文檔)。
如果I s Te x t U n i c o d e 認為緩存包含U n i c o d e 文本,便返回T R U E ,否則返回FA L S E 。確實(shí)是這樣,盡管M i c r o s o f t將該函數的原型規定為返回D W O R D ,但是它實(shí)際上返回一個(gè)布爾值。如果在p R e s u l t 參數指向的整數中必須進(jìn)行特定的測試,該函數就會(huì )在返回之前設定整數中的信息位,以反映每個(gè)測試的結果。
Wi n d o w s 9 8 在Windows 98 下,I s Te x t U n i c o d e 函數沒(méi)有有用的實(shí)現代碼,它只是返回FA L S E 。調用G e t L a s t E r r o r 函數將返回E R R O R _ C A L L _ N O T _ I M P L E M E N T D 。
第1 7 章中的Flie Rev 示例應用程序演示了I s TextUnicode 函數的使用。
2.9.4 在Unicode與ANSI之間轉換字符串
Wi n d o w s 函數M u l t i B y t e To Wi d e C h a r 用于將多字節字符串轉換成寬字符串。下面顯示了M u l t i B y t e To Wi d e C h a r 函數。
int MultiByteToWideChar(
UINT CodePage, //code page
DWORD dwFlags, //character-type options
LPCSTR lpMultiByteStr, //address of string to map
int cchMultiByte, //number of bytes in string
LPWSTR lpWideCharStr, //address of wide-character buffer
int cchWideChar //size of buffer
);
u C o d e P a g e 參數用于標識一個(gè)與多字節字符串相關(guān)的代碼頁(yè)號。d w F l a g s 參數用于設定另一個(gè)控件,它可以用重音符號之類(lèi)的區分標記來(lái)影響字符。這些標志通常并不使用,在d w F l a g s參數中傳遞0 。p M u l t i B y t e S t r 參數用于設定要轉換的字符串,c c h M u l t i B y t e 參數用于指明該字符串的長(cháng)度(按字節計算)。如果為c c h M u l t i B y t e 參數傳遞- 1 ,那么該函數用于確定源字符串的長(cháng)度。
轉換后產(chǎn)生的U n i c o d e 版本字符串將被寫(xiě)入內存中的緩存,其地址由p Wi d e C h a r S t r 參數指定。必須在c c h Wi d e C h a r 參數中設定該緩存的最大值(以字符為計量單位)。如果調用M u l t i B y t e To Wi d e C h a r ,給c c h Wi d e C h a r 參數傳遞0 ,那么該參數將不執行字符串的轉換,而是返回為使轉換取得成功所需要的緩存的值。一般來(lái)說(shuō),可以通過(guò)下列步驟將多字節字符串轉換成U n i c o d e 等價(jià)字符串:
1) 調用M u l t i B y t e To Wi d e C h a r 函數,為p Wi d e C h a r S t r 參數傳遞N U L L ,為c c h Wi d e C h a r 參數傳遞0 。
2) 分配足夠的內存塊,用于存放轉換后的U n i c o d e 字符串。該內存塊的大小由前面對M u l t B y t e To Wi d e C h a r 的調用返回。
3) 再次調用M u l t i B y t e To Wi d e C h a r ,這次將緩存的地址作為p Wi d e C h a r S t r 參數來(lái)傳遞,并傳遞第一次調用M u l t i B y t e To Wi d e C h a r 時(shí)返回的緩存大小,作為c c h Wi d e c h a r 參數。
4. 使用轉換后的字符串。
5) 釋放U n i c o d e 字符串占用的內存塊。
函數Wi d e C h a r To M u l t i B y t e 將寬字符串轉換成等價(jià)的多字節字符串,如下所示:
int WideCharToMultiByte(
UINT CodePage, // code page
DWORD dwFlags, // performance and mapping flags
LPCWSTR lpWideCharStr, // address of wide-character string
int cchWideChar, // number of characters in string
LPSTR lpMultiByteStr, // address of buffer for new string
int cchMultiByte, // size of buffer
LPCSTR lpDefaultChar, // address of default for unmappable
// characters
LPBOOL lpUsedDefaultChar // address of flag set when default
// char. used
);
該函數與M u l t i B i t e To Wi d e C h a r 函數相似。同樣,u C o d e P a g e 參數用于標識與新轉換的字符串相關(guān)的代碼頁(yè)。d w F l a g s 則設定用于轉換的其他控件。這些標志能夠作用于帶有區分符號的字符和系統不能轉換的字符。通常不需要為字符串的轉換而擁有這種程度的控制手段,你將為d w F l a g s 參數傳遞0 。
p Wi d e C h a r S t r 參數用于設定要轉換的字符串的內存地址,c c h Wi d e C h a r 參數用于指明該字符串的長(cháng)度(用字符數來(lái)計量)。如果你為c c h Wi d e C h a r 參數傳遞- 1 ,那么該函數用于確定源字符串的長(cháng)度。
轉換產(chǎn)生的多字節版本的字符串被寫(xiě)入由p M u l t i B y t e S t r 參數指明的緩存。必須在c c h M u l t i B y t e參數中設定該緩存的最大值(用字節來(lái)計量)。如果傳遞0 作為Wi d e C h a r To M u l t i B y t e 函數的c c h M u l t i B y t e 參數,那么該函數將返回目標緩存需要的大小值。通??梢允褂脤⒍嘧止澴址D換成寬字節字符串時(shí)介紹的一系列類(lèi)似的事件,將寬字節字符串轉換成多字節字符串。
你會(huì )發(fā)現,Wi d e C h a r To M u l t i B y t e 函數接受的參數比M u l t i B y t e To Wi d e C h a r 函數要多2 個(gè),即p D e f a u l t C h a r 和p f U s e d D e f a u l t C h a r 。只有當Wi d e C h a r To M u l t i B y t e 函數遇到一個(gè)寬字節字符,而該字符在u C o d e P a g e 參數標識的代碼頁(yè)中并沒(méi)有它的表示法時(shí),Wi d e C h a r To M u l t i B y t e 函數才使用這兩個(gè)參數。如果寬字節字符不能被轉換,該函數便使用p D e f a u l t C h a r 參數指向的字符。如果該參數是N U L L (這是大多數情況下的參數值),那么該函數使用系統的默認字符。該默認字符通常是個(gè)問(wèn)號。這對于文件名來(lái)說(shuō)是危險的,因為問(wèn)號是個(gè)通配符。
p f U s e d D e f a u l t C h a r 參數指向一個(gè)布爾變量,如果寬字符串中至少有一個(gè)字符不能轉換成等價(jià)多字節字符,那么函數就將該變量置為T(mén) R U E 。如果所有字符均被成功地轉換,那么該函數就將該變量置為FA L S E 。當函數返回以便檢查寬字節字符串是否被成功地轉換后,可以測試該變量。同樣,通常為該測試傳遞N U L L 。
關(guān)于如何使用這些函數的詳細說(shuō)明,請參見(jiàn)Platform SDK 文檔。
如果使用這兩個(gè)函數,就可以很容易創(chuàng )建這些函數的U n i c o d e 版本和A N S I 版本。例如,你可能有一個(gè)動(dòng)態(tài)鏈接庫,它包含一個(gè)函數,能夠轉換字符串中的所有字符??梢韵裣旅孢@樣編寫(xiě)該函數的U n i c o d e 版本:
BOOL StringReverseW(PWSTR pWideCharStr)
{
//Get a pointer to the last character in the string.
PWSTR pEndOfStr=pWideCharStr+wcslen(pWideCharStr)-1;
wchar_t cCharT;
//Repeat until we reach the center character
//in the string.
while (pWideCharStr < pEndOfStr)
{
//Save a character in a temporary variable.
cCharT=*pWideCharStr;
//Put the last character in the first character.
*pWideCharStr =*pEndOfStr;
//Put the temporary character in the last character.
*pEndOfStr=cCharT;
//Move in one character from the left.
pWideCharStr++;
//Move in one character from the right.
pEndOfStr--;
}
//The string is reversed; return success.
return(TRUE);
}
你可以編寫(xiě)該函數的A N S I 版本以便該函數根本不執行轉換字符串的實(shí)際操作。你也可以編寫(xiě)該函數的A N S I 版本,以便該函數它將A N S I 字符串轉換成U n i c o d e 字符串,將U n i c o d e 字符串傳遞給S t r i n g R e v e r s e W 函數,然后將轉換后的字符串重新轉換成A N S I 字符串。該函數類(lèi)似下面的樣子:
BOOL StringReverseA(PSTR pMultiByteStr)
{
PWSTR pWideCharStr;
int nLenOfWideCharStr;
BOOL fOk = FALSE;
//Calculate the number of characters needed to hold
//the wide_character version of string.
nLenOfWideCharStr = MultiRyteToWideChar(CP_ACP, 0,
pMultiByteStr, -1, NULL, 0);
//Allocate memory from the process's default heap to
//accommodate the size of the wide-character string.
//Don't forget that MultiByteToWideChar returns the
//number of characters,not the number of bytes,so
//you must multiply by the size of wide character.
pWideCharStr = HeapAlloc(GetProcessHeap(), 0,
nLenOfWideCharStr * sizeof(WCHAR));
if (pWideCharStr == NULL)
return(fOk);
//Convert the multibyte string to a wide_character string.
MultiByteToWideChar(CP_ACP, 0, pMulti8yteStr, -1,
pWideCharStr, nLenOfWideCharStr);
//Call the wide-character version of this
//function to do the actual work
fOk = StnngReverseW(pWideCharStr);
if (fOk)
{
//Convert the wide-character string back
//to a multibyte string.
WideCharToMultiByte(CP_ACP, 0, pWideCharStr, -1,
pMultiByteStr, strlen(pMultiByteStr), NULL, NULL);
}
//Free the momory containing the wide-character string.
HeapFree(GetProcessHeap(), 0, pWideCharStr);
return(fOk),
}
最后,在用動(dòng)態(tài)鏈接庫分配的頭文件中,可以像下面這樣建立這兩個(gè)函數的原型:
BOOL StringReverseW (PWSTR pWideCharStr);
BOOL StringReverseA (PSTR pMultiByteStr);
#ifdef UNICODE
#define StnngReverse StringReverseW
#else
#define StringRevcrsc StringReverseA
#endif // UNICODE
第3章 內核對象
在介紹Windows API 的時(shí)候,首先要講述內核對象以及它們的句柄。本章將要介紹一些比較抽象的概念,在此并不討論某個(gè)特定內核對象的特性,相 反只是介紹適用于所有內核對象的特性。
首先介紹一個(gè)比較具體的問(wèn)題,準確地理解內核對象對于想要成為一名Wi n d o w s 軟件開(kāi)發(fā)能手的人來(lái)說(shuō)是至關(guān)重要的。內核對象可以供系統和 應用程序使用來(lái)管理各種各樣的資源,比如進(jìn)程、線(xiàn)程和文件等。本章講述的概念也會(huì )出現在本書(shū)的其他各章之中。但是,在你開(kāi)始使用實(shí)際的函數 來(lái)操作內核對象之前,是無(wú)法深刻理解本章講述的部分內容的。因此當閱讀本書(shū)的其他章節時(shí),可能需要經(jīng)?;剡^(guò)來(lái)參考本章的內容。
3.1 什么是內核對象
作為一個(gè)Wi n d o w s 軟件開(kāi)發(fā)人員,你經(jīng)常需要創(chuàng )建、打開(kāi)和操作各種內核對象。系統要創(chuàng )建和操作若干類(lèi)型的內核對象,比如存取符號對象、 事件對象、文件對象、文件映射對象、I / O 完成端口對象、作業(yè)對象、信箱對象、互斥對象、管道對象、進(jìn)程對象、信標對象、線(xiàn)程對象和等待計 時(shí)器對象等。這些對象都是通過(guò)調用函數來(lái)創(chuàng )建的。例如,C r e a t e F i l e M a p p i n g 函數可使系統能夠創(chuàng )建一個(gè)文件映射對象。每個(gè)內 核對象只是內核分配的一個(gè)內存塊,并且只能由該內核訪(fǎng)問(wèn)。該內存塊是一種數據結構,它的成員負責維護該對象的各種信息。有些數據成員(如安全性描述符、使用計數等)在所有對象類(lèi)型中是相同的,但大多數數據成員屬于特定的對象類(lèi)型。例如,進(jìn)程對象有一個(gè)進(jìn)程I D 、一個(gè)基 本優(yōu)先級和一個(gè)退出代碼,而文件對象則擁有一個(gè)字節位移、一個(gè)共享模式和一個(gè)打開(kāi)模式。
由于內核對象的數據結構只能被內核訪(fǎng)問(wèn),因此應用程序無(wú)法在內存中找到這些數據結構并直接改變它們的內容。M i c r o s o f t 規定了這個(gè)限 制條件,目的是為了確保內核對象結構保持狀態(tài)的一致。這個(gè)限制也使M i c r o s o f t 能夠在不破壞任何應用程序的情況下在這些結構中添加、 刪除和修改數據成員。
如果我們不能直接改變這些數據結構,那么我們的應用程序如何才能操作這些內核對象呢?解決辦法是,Wi n d o w s 提供了一組函數,以便用定 義得很好的方法來(lái)對這些結構進(jìn)行操作。這些內核對象始終都可以通過(guò)這些函數進(jìn)行訪(fǎng)問(wèn)。當調用一個(gè)用于創(chuàng )建內核對象的函數時(shí),該函數就返回一 個(gè)用于標識該對象的句柄。該句柄可以被視為一個(gè)不透明值,你的進(jìn)程中的任何線(xiàn)程都可以使用這個(gè)值。將這個(gè)句柄傳遞給Wi n d o w s 的各個(gè)函 數,這樣,系統就能知道你想操作哪個(gè)內核對象。本章后面還要詳細講述這些句柄的特性。
為了使操作系統變得更加健壯,這些句柄值是與進(jìn)程密切相關(guān)的。因此,如果將該句柄值傳遞給另一個(gè)進(jìn)程中的一個(gè)線(xiàn)程(使用某種形式的進(jìn)程間的 通信)那么這另一個(gè)進(jìn)程使用你的進(jìn)程的句柄值所作的調用就會(huì )失敗。在3 . 3 節“跨越進(jìn)程邊界共享內核對象”中,將要介紹3 種機制,使多個(gè)進(jìn) 程能夠成功地共享單個(gè)內核對象。
3.1.1 內核對象的使用計數
內核對象由內核所擁有,而不是由進(jìn)程所擁有。換句話(huà)說(shuō),如果你的進(jìn)程調用了一個(gè)創(chuàng )建內核對象的函數,然后你的進(jìn)程終止運行,那么內核對象不 一定被撤消。在大多數情況下,對象將被撤消,但是如果另一個(gè)進(jìn)程正在使用你的進(jìn)程創(chuàng )建的內核對象,那么該內核知道,在另一個(gè)進(jìn)程停止使用該 對象前不要撤消該對象,必須記住的是,內核對象的存在時(shí)間可以比創(chuàng )建該對象的進(jìn)程長(cháng)。
內核知道有多少進(jìn)程正在使用某個(gè)內核對象,因為每個(gè)對象包含一個(gè)使用計數。使用計數是所有內核對象類(lèi)型常用的數據成員之一。當一個(gè)對象剛剛 創(chuàng )建時(shí),它的使用計數被置為1 。然后,當另一個(gè)進(jìn)程訪(fǎng)問(wèn)一個(gè)現有的內核對象時(shí),使用計數就遞增1 。當進(jìn)程終止運行時(shí),內核就自動(dòng)確定該進(jìn)程 仍然打開(kāi)的所有內核對象的使用計數。如果內核對象的使用計數降為0 ,內核就撤消該對象。這樣可以確保在沒(méi)有進(jìn)程引用該對象時(shí)系統中不保留任 何內核對象。
3.1.2 安全性
內核對象能夠得到安全描述符的保護。安全描述符用于描述誰(shuí)創(chuàng )建了該對象,誰(shuí)能夠訪(fǎng)問(wèn)或使用該對象,誰(shuí)無(wú)權訪(fǎng)問(wèn)該對象。安全描述符通常在編寫(xiě) 服務(wù)器應用程序時(shí)使用,如果你編寫(xiě)客戶(hù)機端的應用程序,那么可以忽略?xún)群藢ο蟮倪@個(gè)特性。
Windows 98 根據原來(lái)的設計,Windows 98 并不用作服務(wù)器端的操作系統。為此,M i c r o s o f t 公司沒(méi)有在Windows 98 中配備安全特性。不 過(guò),如果你現在為Windows 98設計軟件,在實(shí)現你的應用程序時(shí)仍然應該了解有關(guān)的安全問(wèn)題,并且使用相應的訪(fǎng)問(wèn)信息,以確保它能在Windows 2000上正確地運行.
用于創(chuàng )建內核對象的函數幾乎都有一個(gè)指向S E C U R I T Y _ AT T R I B U T E S 結構的指針作為其參數,下面顯示了C r e a t e F i l e M a p p i n g 函數的指針:
HANDLE CreateFileMapping(
HANDLE hFile.
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximuniSizeLow,
PCTSTR pszNarne);
大多數應用程序只是為該參數傳遞N U L L ,這樣就可以創(chuàng )建帶有默認安全性的內核對象。默認安全性意味著(zhù)對象的管理小組的任何成員和對象的創(chuàng ) 建者都擁有對該對象的全部訪(fǎng)問(wèn)權,而其他所有人均無(wú)權訪(fǎng)問(wèn)該對象。但是,可以指定一個(gè)S E C U R I T Y _ AT T R I B U T E S 結構,對它進(jìn) 行初始化,并為該參數傳遞該結構的地址。S E C U R I T Y _ AT T R I B U T E S 結構類(lèi)似下面的樣子:盡管該結構稱(chēng)為S E C U R I T Y _ AT T R I B U T E S ,但是它包含的與安全性有關(guān)的成員實(shí)際上只有一個(gè),即l p S e c u r i t y D e s c r i p t o r 。如果你想要限制人們對你 創(chuàng )建的內核對象的訪(fǎng)問(wèn),必須創(chuàng )建一個(gè)安全性描述符,然后像下面這樣對S E C U R I T Y _ AT T R I B U T E S 結構進(jìn)行初始化:
typedef struct _SECURITY_ATTRIBUTES
{
DWORD nLength,
LPVOID lpSecurityDescriptor;
BOOL bInherttHandle;
} SECURITY_ATTRIBUTES;
盡管該結構稱(chēng)為S E C U R I T Y _ AT T R I B U T E S ,但是它包含的與安全性有關(guān)的成員實(shí)際上只有一個(gè),即l p S e c u r i t y D e s c r i p t o r 。如果你想要限制人們對你創(chuàng )建的內核對象的訪(fǎng)問(wèn),必須創(chuàng )建一個(gè)安全性描述符,然后像下面這樣對S E C U R I T Y _ AT T R I B U T E S 結構進(jìn)行初始化:
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa); //Used for versioning
sa.lpSecuntyDescriptor = pSD, //Address of an initialized SD
sa.bInheritHandle = FALSE; //Discussed later
HANDLE hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE,
&sa, PAGE_REAOWRITE, 0, 1024, "MyFileMapping");
由于b I n h e r i t H a n d l e 這個(gè)成員與安全性毫無(wú)關(guān)系,因此準備推遲到本章后面部分繼承性一節中再介紹b I n h e r i t H a n d l e 這個(gè)成員。
當你想要獲得對相應的一個(gè)內核對象的訪(fǎng)問(wèn)權(而不是創(chuàng )建一個(gè)新對象)時(shí),必須設定要對該對象執行什么操作。例如,如果想要訪(fǎng)問(wèn)一個(gè)現有的文 件映射內核對象,以便讀取它的數據,那么應該調用下面這個(gè)O p e n f i l e M a p p i n g 函數:
HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ,
FALSE, "MyFileMapping");
通過(guò)將F I L E _ M A P _ R E A D 作為第一個(gè)參數傳遞給O p e n F i l e M a p p i n g ,指明打算在獲得對該文件映象的訪(fǎng)問(wèn)權后讀取該文件 ,O p e n F i l e M a p p i n g 函數在返回一個(gè)有效的句柄值之前,首先執行一次安全檢查。如果(已登錄用戶(hù))被允許訪(fǎng)問(wèn)現有的文件映射內 核對象,O p e n F i l eM a p p i n g 就返回一個(gè)有效的句柄。但是,如果被拒絕訪(fǎng)問(wèn)該對象,O p e n F i l e M a p p i n g 將返回N U L L ,而調用G e t L a s t E r r o r 函數則返回5 (E R R O R _ A C C E S S _ D E N I E D ),同樣,大多數應用程序并不使用該安全性,因此 將不進(jìn)一步討論這個(gè)問(wèn)題。
Windows 98 雖然許多應用程序不需要考慮安全性問(wèn)題,但是Wi n d o w s 的許多函數要求傳遞必要的安全訪(fǎng)問(wèn)信息。為Windows 98 設計的若干應 用程序在Windows 2000 上無(wú)法正確地運行,因為在實(shí)現這些應用程序時(shí)沒(méi)有對安全問(wèn)題給于足夠的考慮。
例如,假設一個(gè)應用程序在開(kāi)始運行時(shí)要從注冊表的子關(guān)鍵字中讀取一些數據。為了正確地進(jìn)行這項操作,你的代碼應該調用R e g O p e n K e y E x ,傳遞K E Y_Q U E RY_VA L U E ,以便獲得必要的訪(fǎng)問(wèn)權。
但是,許多應用程序原先是為Windows 98 開(kāi)發(fā)的,當時(shí)沒(méi)有考慮到運行Wi n d o w s2 0 0 0 的需要。由于Windows 98 沒(méi)有解決注冊表的安全問(wèn)題 ,因此軟件開(kāi)發(fā)人員常常要調用R e g O p e n K e y E x 函數,傳遞K E Y _ A l l _ A C C E S S ,作為必要的訪(fǎng)問(wèn)權。開(kāi)發(fā)人員這樣做的原因 是,它是一種比較簡(jiǎn)單的解決方案,意味著(zhù)開(kāi)發(fā)人員不必考慮究竟需要什么訪(fǎng)問(wèn)權。問(wèn)題是注冊表的子關(guān)鍵字可以被用戶(hù)讀取,但是不能寫(xiě)入。
因此,當該應用程序現在放在Windows 2000 上運行時(shí),用K E Y _ A L L _ A C C E S S 調用R e g O p e n K e y E x 就會(huì )失敗,而且,沒(méi)有相 應的錯誤檢查方法,應用程序的運行就會(huì )產(chǎn)生不可預料的結果。
如果開(kāi)發(fā)人員想到安全問(wèn)題,把K E Y _ A L L _ A C C E S S 改為K E Y _ Q U E RY _ VA L U E ,則該產(chǎn)品可適用于兩種操作系統平臺。
開(kāi)發(fā)人員的最大錯誤之一就是忽略安全訪(fǎng)問(wèn)標志。使用正確的標志會(huì )使最初為Windows 98 設計的應用程序更易于向Windows 2000 轉換。
除了內核對象外,你的應用程序也可以使用其他類(lèi)型的對象,如菜單、窗口、鼠標光標、刷子和字體等。這些對象屬于用戶(hù)對象或圖形設備接口(G D I )對象,而不是內核對象。當初次著(zhù)手為Wi n d o w s 編程時(shí),如果想要將用戶(hù)對象或G D I 對象與內核對象區分開(kāi)來(lái),你一定會(huì )感到不知所 措。比如,圖標究竟是用戶(hù)對象還是內核對象呢?若要確定一個(gè)對象是否屬于內核對象,最容易的方法是觀(guān)察創(chuàng )建該對象所用的函數。創(chuàng )建內核對象 的所有函數幾乎都有一個(gè)參數,你可以用來(lái)設定安全屬性的信息,這與前面講到的C r e a t e F i l e M a p p i n g 函數是相同的。
用于創(chuàng )建用戶(hù)對象或G D I 對象的函數都沒(méi)有P S E C U R I T Y _ AT T R I B U T E S 參數。例如,讓我們來(lái)看一看下面這個(gè)C r e a t e I c o n 函數:
HICON CreateIcon(
HINSTANCE hinst.
int nWidth,
int nHeight,
BYTE cPlanes,
BYTE cBitsPixel,
CONST BYTE *pbANDbits,
CONST BYTE *pbXORbits);
3.2 進(jìn)程的內核對象句柄表
當一個(gè)進(jìn)程被初始化時(shí),系統要為它分配一個(gè)句柄表。該句柄表只用于內核對象,不用于用戶(hù)對象或G D I 對象。句柄表的詳細結構和管理方法并沒(méi)有 具體的資料說(shuō)明。通常我并不介紹操作系統中沒(méi)有文檔資料的那些部分。不過(guò),在這種情況下,我會(huì )進(jìn)行例外處理,因為,作為一個(gè)稱(chēng)職的Wi n d o w s 程序員,必須懂得如何管理進(jìn)程的句柄表。由于這些信息沒(méi)有文檔資料,因此不能保證所有的詳細信息都正確無(wú)誤,同時(shí),在Windows 2000 Windows 98 和Windows CE 中,它們的實(shí)現方法是不同的。為此,請認真閱讀下面介紹的內容以加深理解,在此不學(xué)習系統是如何進(jìn)行操作的。
表3 -

