打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
WIDOWS核心资料(全)
userphoto

2008.12.20

关注
WIDOWS核心资料(全)[
[hac-ker 发表于 2007-1-15 19:16:00]
第一部分 程序员必读 

第1章 对程序错误的处理 
在开始介绍Microsoft Windows 的特性之前,必须首先了解Windows的各个函数是如何进行错误处理的。 
当调用一个Windows函数时,它首先要检验传递给它的的各个参数的有效性,然后再设法执行任务。如果传递了一个无效参数,或者由于某种原因无法执行这项操作,那么操作系统就会返回一个值,指明该函数在某种程度上运行失败了。表1 - 1列出了大多数Windows函数使用的返回值的数据类型。 
表1-1 Wi n d o w s 函数常用的返回值类型 
数据类型     表示失败的值 
V O I D     该函数的运行不可能失败。Wi n d o w s 函数的返回值类型很少是V O I D 
B O O L    如果函数运行失败,那么返回值是0 ,否则返回的是非0 值。最好对返回值进行测试,以确定它是0 还是非0 。不要测试返回值是否为T R U E 
H A N D L E    如果函数运行失败,则返回值通常是N U L L ,否则返回值为H A N D L E ,用于标识你可以操作的一个对象。注意,有些函数会返回一个句柄值I N VALID_ HANDLE_VA L U E ,它被定义为- 1 。函数的Platform SDK 文档将会清楚地说明该函数运行失败时返回的是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     这是个难以处理的值。返回数量的函数通常返回L O N G 或D W O R D 。如果由于某种原因,函数无法对想要进行计数的对象进行计数,那么该函数通常返回0 或- 1 (根据函数而定)。如果调用的函数返回了L O N G / D W O R D ,那么请认真阅读Platform SDK文档,以确保能正确检查潜在的错误 
一个Wi n d o w s 函数返回的错误代码对了解该函数为什么会运行失败常常很有用。M i c r o s o f t公司编译了一个所有可能的错误代码的列表,并且为每个错误代码分配了一个3 2 位的号码。 
从系统内部来讲,当一个Wi n d o w s 函数检测到一个错误时,它会使用一个称为线程本地存储器(thread-local storage )的机制,将相应的错误代码号码 与调用的线程关联起来(线程本地存储器将在第2 1 章中介绍)。这将使线程能够互相独立地运行,而不会影响各自的错误代码。当函数返回时,它的返回值 就能指明一个错误已经发生。若要确定这是个什么错误,请调用G e t L a s t E r r o r 函数: 
DWORD GetLastError(); 
该函数只返回线程的3 2 位错误代码。 
当你拥有3 2 位错误代码的号码时,必须将该号码转换成更有用的某种对象。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 
如你所见,每个错误都有3 种表示法:一个消息I D (这是你可以在源代码中使用的一个宏,以便与G e t L a s t E r r o r 的返回值进行比较),消息文本(对错误的英文描述)和一个号码(应该避免使用这个号码,可使用消息I D )。请记住,这里只显示了Wi n E r r o r. h 头文件中的很少一部分内容,整个文件的长度超过2 1 0 0 0 行。 
当Wi n d o w s 函数运行失败时,应该立即调用G e t L a s t E r r o r 函数。如果调用另一个Wi n d o w s 函数,它的值很可能被改写。 
注意G e t L a s t E r r o r 能返回线程产生的最后一个错误。如果该线程调用的Wi n d o w s 函数运行成功,那么最后一个错误代码就不被改写,并且不指明运行成功。有少数Wi n d o w s 函数并不遵循这一规则,它会更改最后的错误代码;但是Platform SDK 文档通常指明,当函数运行成功时,该函数会更改最后的错误代码。 
Wi n d o w s 9 8 许多Windows 98 的函数实际上是用M i c r o s o f t 公司的1 6 位Windows 3.1 产品产生的1 6 位代码来实现的。这种比较老的代码并 不通过G e t L a s t E r r o r 之类的函数来报告错误,而且M i c r o s o f t 公司并没有在Windows 98 中修改1 6 位代码,以支持这种错误处理方式 。对于我们来说,这意味着Windows 98 中的许多Wi n 3 2 函数在运行失败时不能设置最后的错误代码。该函数将返回一个值,指明运行失败,这样你就能够 发现该函数确实已经运行失败,但是你无法确定运行失败的原因。 
有些Wi n d o w s 函数之所以能够成功运行,其中有许多原因。例如,创建指明的事件内核对象之所以能够取得成功,是因为你实际上创建了该对象,或者因为已经存在带有相同名字的事件内核对象。你应搞清楚成功的原因。为了将该信息返回,M i c r o s o f t 公司选择使用最后错误代码机制。这样,当某些函数运行成功时,就能够通过调用G e t L a d t E r r o r 函数来确定其他的一些信息。对于具有这种行为特性的函数来说,Platform SDK 文档清楚地说明了G e t L a s t E r r o r 函数可以这样使用。请参见该文档,找出C r e a t e E v e n t 函数的例子。 
进行调试的时候,监控线程的最后错误代码是非常有用的。在Microsoft Visual studio 6.0 中,M i c r o s o f t 的调试程序支持一个非常有用的特性,即可以配置Wa t c h 窗口,以便始终都能显示线程的最后错误代码的号码和该错误的英文描述。通过选定Wa t c h 窗口中的一行,并键入“@ e r r, h r ”,就能够做到这一点。观察图1 - 1 ,你会看到已经调用了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 ,表示它未能打开指定的文件。但是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 窗口又进一步指明错误代码2 是指“系统不能找到指定的文件。”你会发现它与Wi n E r r o r. h 头文件中的错误代码2 所指的字符串是相同的。 

图1-1 在Visual Studio 6.0 的Wa t c h 窗口中键入“@ e r r, h r ”,就可以查看当前线程的最后错误代码 
Visual studio 还配有一个小的实用程序,称为Error Lookup 。可以使用Error Lookup将错误代码的号码转换成相应文本描述(见图1 - 2 )。 

图1-2 Error Lookup 窗口 
如果在编写的应用程序中发现一个错误,可能想要向用户显示该错误的文本描述。Wi n d o w s 提供了一个函数,可以将错误代码转换成它的文本描述。该函数称为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 函数的功能实际上是非常丰富的,在创建向用户显示的字符串信息时,它是首选函数。该函数之所以有这样大的作用,原因之一 是它很容易用多种语言进行操作。该函数能够检测出用户首选的语言(在Regional Settings Control Panel 小应用程序中设定),并返回相应的文本。当然 ,首先必须自己转换字符串,然后将已转换的消息表资源嵌入你的. e x e 文件或D L L 模块中,然后该函数会选定正确的嵌入对象。E r r o r S h o w 示 例应用程序(本章后面将加以介绍)展示了如何调用该函数,以便将M i c r o s o f t 公司定义的错误代码转换成它的文本描述。 
有些人常常问我,M i c r o s o f t 公司是否建立了一个主控列表,以显示每个Wi n d o w s 函数可能返回的所有错误代码。可惜,回答是没有这样的列 表,而且M i c r o s o f t 公司将永远不会建立这样的一个列表。因为在创建系统的新版本时,建立和维护该列表实在太困难了。 
建立这样一个列表存在的问题是,你可以调用一个Wi n d o w s 函数,但是该函数能够在内部调用另一个函数,而这另一个函数又可以调用另一个函数,如 此类推。由于各种不同的原因,这些函数中的任何一个函数都可能运行失败。有时,当一个函数运行失败时,较高级的函数对它进行恢复,并且仍然可以执行 你想执行的操作。为了创建该主控列表,M i c r o s o f t 公司必须跟踪每个函数的运行路径,并建立所有可能的错误代码的列表。这项工作很困难。而且 ,当创建系统的新版本时,这些函数的运行路径还会改变。 

1.1 定义自己的错误代码 
前面已经说明Wi n d o w s 函数是如何向函数的调用者指明发生的错误,你也能够将该机制用于自己的函数。比如说,你编写了一个希望其他人调用的函数,你的函数可能因为这样或那样的原因而运行失败,你必须向函数的调用者说明它已经运行失败。 
若要指明函数运行失败,只需要设定线程的最后的错误代码,然后让你的函数返回FA L S E 、I N VA L I D _ H A N D L E _ VA L U E 、N U L L 或者返回任何合适的信息。若要设定线程的最后错误代码,只需调用下面的代码: 
请将你认为合适的任何3 2 位号码传递给该函数。尝试使用Wi n E r r o r. h 中已经存在的代码, 
VOID SetLastError(DWORD dwErrCode); 
只要该代码能够正确地指明想要报告的错误即可。如果你认为Wi n E r r o r. h 中的任何代码都不能正确地反映该错误的性质,那么可以创建你自己的代码 。错误代码是个3 2 位的数字,划分成表1-2所示的各个域。 
表1-2 错误代码的域 
位    3 1 ~30    29    28    27~16    15~0 
内容    严重性    M i c r o s o f t/客户    保留    设备代码    异常代码 
含义    0 =成功    0 =M i c r o s o f t公司定义的代码    必须是0    由M i c r o s o f t公司定义    由Microsoft/客户定义 
     1 =供参考    1 =客户定义的代码                
     2 =警告                     
     3 =错误                     
这些域将在第2 4 章中详细讲述。现在,需要知道的重要域是第2 9 位。M i c r o s o f t 公司规定,他们建立的所有错误代码的这个信息位均使用0 。如果创建自己的错误代码,必须使2 9 位为1 。这样,就可以确保你的错误代码与M i c r o s o f t 公司目前或者将来定义的错误代码不会发生冲突。 
  
1.2 ErrorShow示例应用程序 
E r r o r S h o w 应用程序“01 ErrorShow. e x e ”(在清单1 - 1 中列出)展示了如何获取错误代码的文本描述的方法。该应用程序的源代码和资源文件位于本书所附光盘上的0 1 - E r r o r S h o w 目录下。一般来说,该应用程序用于显示调试程序的Wa t c h 窗口和Error Lookup 程序是如何运行的。当启动该程序时,就会出现如图1 - 3 所示的窗口。 

图1-3 Error Show 窗口 
可以将任何错误代码键入该编辑控件。当单击Look up 按钮时,在底部的滚动窗口中就会显示该错误的文本描述。该应用程序唯一令人感兴趣的特性是如何调用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.")); 
第一个代码行用于从编辑控件中检索错误代码的号码。然后,建立一个内存块的句柄并将它初始化为N U L L 。F o r m a t M e s s a g e 函数在内部对内存块进行分配,并将它的句柄返回给我们。 
当调用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 _ 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 变量中返回。第三个参数指明想要查找的错误代码的号码,第四个参数指明想要文本描述使用什么语言。 
如果F o r m a t M e s s a g e 函数运行成功,那么错误代码的文本描述就位于内存块中,将它拷贝到对话框底部的滚动窗口中。如果F o r m a t M e s s a g e 函数运行失败,设法查看N e t M s g . d l l 模块中的消息代码,以了解该错误是否与网络有关。使用N e t M s g . d l l 模块的句柄,再次调用F o r m a t M e s s a g e 函数。你会看到,每个D L L (或. e x e )都有它自己的一组错误代码,可以使用Message Compiler (M C . e x e )将这组错误代码添加给该模块,并将一个资源添加给该模块。这就是Visual Studio 的Error Lookup 工具允许你用M o d u l e s对话框进行的操作。以下是清单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 
随着M i c r o s o f t 公司的Wi n d o w s 操作系统在全世界日益广泛的流行,对于软件开发人员来说,将目标瞄准国际上的各个不同市场,已经成为一个越来越重要的问题。美国的软件版本比国际版本提前6 个月推向市场,这曾经是个司空见惯的现象。但是,由于各国对Wi n d o w s 操作系统提供了越来越多的支持,因此就更加容易为国际市场生产各种应用软件,从而缩短了软件的美国版本与国际版本推出的时间间隔。 
Wi n d o w s 操作系统始终不逾地提供各种支持,以帮助软件开发人员进行应用程序的本地化工作。应用软件可以从各种不同的函数中获得特定国家的信息,并可观察控制面板的设置,以确定用户的首选项。Wi n d o w s 甚至支持不同的字体,以适应应用的需要。 
之所以将这一章放在本书的开头,是因为考虑到U n i c o d e 是开发任何应用程序时要采用的基本步骤。本书的每一章中几乎都要讲到关于U n i c o d e 的问题,而且书中给出的所有示例应用程序都是“用U n i c o d e 实现的”。如果你为Microsoft Windows 2000 或Microsoft Windows CE 开发应用程序,你应该使用U n i c o d e 进行开发。如果你为Microsoft Windows 98 开发应用程序,你必须对某些问题作出决定。本章也要讲述Windows 98 的有关问题。 

2.1 字符集 
软件的本地化要解决的真正问题,实际上就是如何来处理不同的字符集。多年来,许多人一直将文本串作为一系列单字节字符来进行编码,并在结尾处放上一个零。对于我们来说,这已经成了习惯。当调用s t r l e n 函数时,它在以0 结尾的单字节字符数组中返回字符的数目。 
问题是,有些文字和书写规则(比如日文中的汉字就是个典型的例子)的字符集中的符号太多了,因此单字节(它提供的符号最多不能超过2 5 6 个)是根本不敷使用的。为此出现了双字节字符集(D B C S ),以支持这些文字和书写规则。 
2.1.1 单字节与双字节字符集 
在双字节字符集中,字符串中的每个字符可以包含一个字节或包含两个字节。例如,日文中的汉字,如果第一个字符在0 x 8 1 与0 x 9 F 之间,或者在0 x E 0 与0 x F C 之间,那么就必须观察下一个字节,才能确定字符串中的这个完整的字符。使用双字节字符集,对于程序员来说简直是个很大的难题,因为有些字符只有一个字节宽,而有些字符则是两个字节宽。 
如果只是调用s t r l e n 函数,那么你无法真正了解字符串中究竟有多少字符,它只能告诉你到达结尾的0 之前有多少个字节。A N S I 的C 运行期库中没有配备相应的函数,使你能够对双字节字符集进行操作。但是,Microsoft Visual C++的运行期库却包含许多函数,如_ m b s l e n ,它可以用来操作多字节(既包括单字节也包括双字节)字符串。 
为了帮助你对D B C S 字符串进行操作,Wi n d o w s 提供了下面的一组帮助函数(见表2 - 1 )。前两个函数CharNext 和Char Prev 允许前向或逆向遍历DBCS 字符串,方法是每次一个字符。第三个函数IsDBCSLeadByte, 在字节返回到一个两字字节符的第一个字节时将返回T R U E 。 
表2-1 对D B C S 字符串进行操作的帮助函数 
函数    描述 
PTSTR CharNext(PCTSTR pszCurrentChar);    返回字符串中的下一个字符的地址 
PTSTR CharPrev (PCTSTR pszStart,PCTSTR p s z C u r r e n t C h a r);    返回字符串中的上一个字符的地址 
BOOL IsDBCSLeadByteTRUE(BYTE bTestChar);    如果该字节是DBCS 字符的第一个字节,则返回 
  
尽管这些函数使得我们对D B C S 的操作更容易,但还需要,一个更好的方法让我们来看看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 年建立的一个技术标准。1 9 9 1 年,成立了一个集团机构负责U n i c o d e 的开发和推广应用。该集团由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 等公司组成(若要了解该集团的全部成员,请通过网址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 》一书(该书可以通过网址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 字符串中的所有字符都是1 6 位的(两个字节)。它没有专门的字节来指明下一个字节是属于同一个字符的组成部分,还是一个新字符。这意味着你只需要对指针进行递增或递减,就可以遍历字符串中的各个字符,不再需要调用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 之类的函数。 
由于U n i c o d e 用一个1 6 位的值来表示每个字符,因此总共可以得到65 000 个字符,这样,它就能够对世界各国的书面文字中的所有字符进行编码,远远超过了单字节字符集的2 5 6 个字符的数目。 
目前,已经为阿拉伯文、中文拼音、西里尔字母(俄文)、希腊文、西伯莱文、日文、韩文和拉丁文(英文)字母定义了U n i c o d e 代码点。(代码点是字符集中符号的位置。)这些字符集中还包含了大量的标点符号、数学符号、技术符号、箭头、装饰标志、区分标志和其他许多字符。如果将所有这些字母和符号加在一起,总计约达3 5 0 0 0 个不同的代码点,这样,总计65 000 多个代码点中,大约还有一半可供将来扩充时使用。 
这65536个字符可以分成不同的区域。表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    梵文 
目前尚未分配的代码点大约还有29 000 个,不过它们是保留供将来使用的。另外,大约有6 0 0 0 个代码点是保留供个人使用的。 

2.2 为什么使用Unicode 
当开发应用程序时,当然应该考虑利用U n i c o d e 的优点。即使现在你不打算对应用程序进行本地化,开发时将U n i c o d e 放在心上,肯定可以简化将来的代码转换工作。此外,U n i c o d e 还具备下列功能: 
• 可以很容易地在不同语言之间进行数据交换。 
• 使你能够分配支持所有语言的单个二进制. e x e 文件或D L L 文件。 
• 提高应用程序的运行效率(本章后面还要详细介绍)。 

2.3 Windows 2000与Unicode 
Windows 2000 是使用U n i c o d e 从头进行开发的,用于创建窗口、显示文本、进行字符串操作等的所有核心函数都需要U n i c o d e 字符串。如果调用任何一个Wi n d o w s 函数并给它传递一个A N S I 字符串,那么系统首先要将字符串转换成U n i c o d e ,然后将U n i c o d e 字符串传递给操作系统。如果希望函数返回A N S I 字符串,系统就会首先将U n i c o d e 字符串转换成A N S I 字符串,然后将结果返回给你的应用程序。所有这些转换操作都是在你看不见的情况下发生的。当然,进行这些字符串的转换需要占用系统的时间和内存。 
例如,如果调用C r e a t e Wi n d o w E x 函数,并传递类名字和窗口标题文本的非U n i c o d e 字符串,那么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 字符串,并将结果存储在分配到的内存块中,然后调用U n i c o d e 版本的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 字符串,然后你的应用程序才能处理该字符串。由于系统必须执行所有这些转换操作,因此你的应用程序需要更多的内存,并且运行的速度比较慢。通过从头开始用U n i c o d e 来开发应用程序,就能够使你的应用程序更加有效地运行。 

2.4 Windows 98与Unicode 
Windows 98 不是一种全新的操作系统。它继承了1 6 位Wi n d o w s 操作系统的特性,它不是用来处理U n i c o d e 的。如果要增加对U n i c o d e 的支持,其工作量非常大,因此在该产品的特性列表中没有包括这个支持项目。由于这个原因,Windows 98 像它的前任产品一样,几乎都是使用A N S I 字符串来进行所有的内部操作的。 
仍然可以编写用于处理U n i c o d e 字符和字符串的Wi n d o w s 应用程序,不过,使用Wi n d o w s 函数要难得多。例如,如果想要调用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 函数并将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 函数返回时,就能释放临时缓存。这比使用Windows 2000 上的U n i c o d e 要麻烦得多。本章的后面要介绍如何在Windows 98 下进行这些转换。 
虽然大多数U n i c o d e 函数在Windows 98 中不起任何作用,但是仍有少数U n i c o d e 函数确实非常有用。这些函数是: 
■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 中会出现各种各样的错误。有些函数无法使用某些字体,有些函数会破坏内存堆栈,有些函数会使打印机驱动程序崩溃,等等。如果要使用这些函数,必须对它们进行大量的测试。即使这样,可能仍然无法解决问题。因此必须向用户说明这些情况。 

2.5 Windows CE与Unicode 
Windows CE 操作系统是为小型设备开发的,这些设备的内存很小,并且不带磁盘存储器。你可能认为,由于M i c r o s o f t 公司的主要目标是建立一种尽可能小的操作系统,因此它会使用A N S I 作为自己的字符集。但是M i c r o s o f t 公司并非鼠目寸光,他们懂得,Windows CE 的设备要在世界各地销售,他们希望降低软件开发成本,这样就能更加容易地开发应用程序。为此,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 开发应用程序,必须懂得U n i c o d e ,并且在整个应用程序中使用U n i c o d e 。 

2.6 需要注意的问题 
下面让我们进一步明确一下“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 ,因此可以为任意一种开发应用程序。 
• Windows 98 只支持A N S I ,只能为A N S I 开发应用程序。 
• Windows CE 只支持U n i c o d e ,只能为U n i c o d e 开发应用程序。 
虽然M i c r o s o f t 公司试图让软件开发人员能够非常容易地开发在这3 种平台上运行的软件,但是U n i c o d e 与A N S I 之间的差异使得事情变得困难起来,并且这种差异通常是我遇到的最大的问题之一。请不要误解,M i c r o s o f t 公司坚定地支持U n i c o d e ,并且我也坚决鼓励你使用它。不过你应该懂得,你可能遇到一些问题,需要一定的时间来解决这些问题。建议你尽可能使用U n i c o d e 。如果运行Windows 98 ,那么只有在必要时才需转换到A N S I 。不过,还有另一个小问题你应该了解,那就是C O M 。 

2.7 对COM的简单说明 
当M i c r o s o f t 公司将C O M 从1 6 位Wi n d o w s 转换成Wi n 3 2 时,公司作出了一个决定,即需要字符串的所有C O M 接口方法都只能接受U n i c o d e 字符串。这是个了不起的决定,因为C O M 通常用于使不同的组件能够互相进行通信,而U n i c o d e 则是传递字符串的最佳手段。 
如果你为Windows 2000 或Windows CE 开发应用程序,并且也使用C O M ,那么你将会如虎添翼。在你的整个源代码中使用U n i c o d e ,将使与操作系统进行通信和与C O M 对象进行通信的操作变成一件轻而易举的事情。 
如果你为Windows 98 开发应用程序,并且也使用C O M ,那么将会遇到一些问题。C O M 要求使用U n i c o d e 字符串,而操作系统的大多数函数要求使用A N S I 字符串。那是多么难办的事情啊!我曾经从事过若干个项目的开发,在这些项目中,我编写了许多代码,仅仅是为了来回进行字符串的转换。 

2.8 如何编写Unicode源代码 
M i c r o s o f t 公司为U n i c o d e 设计了Windows API ,这样,可以尽量减少对你的代码的影响。实际上,你可以编写单个源代码文件,以便使用或者不使用U n i c o d e 来对它进行编译。只需要定义两个宏(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 字符串,定义了一些数据类型。标准的C 头文件S t r i n g . h 已经作了修改,以便定义一个名字为w c h a r _ t 的数据类型,它是一个U n i c o d e 字符的数据类型: 
typedef unsigned short wchar_t; 
例如,如果想要创建一个缓存,用于存放最多为9 9 个字符的U n i c o d e 字符串和一个结尾为零的字符,可以使用下面这个语句: 
wchar_t szBuffer[100]; 
该语句创建了一个由1 0 0 个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 字符串进行操作,不能正确地处理U n i c o d e 字符串。因此,ANSI C 也拥有一组补充函数。清单2 - 1 显示了一些标准的ANSI C 字符串函数,后面是它们的等价U n i c o d e 函数。 
char * strcat(char *,const char *); 
wchar_t * wcscat(wchar_t *,const wchar_t *); 
清单2-1 标准的ANSI C 字符串函数和它们的等价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 开头,w c s 是宽字符串的英文缩写。若要调用U n i c o d e函数,只需用前缀w c s 来取代A N S I 字符串函数的前缀s t r 即可。 
注意大多数软件开发人员可能已经不记得这样一个非常重要的问题了,那就是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 字符和字符串。这意味着始终都可以调用C 运行期函数,以便对U n i c o d e 字符和字符串进行操作,即使是在Windows 98 上运行,也可以调用这些函数。换句话说,w c s c a t 、w c s l e n 和w c s t o k 等函数都能够在Windows 98 上很好地运行,这些都是必须关心的操作系统函数。 
对于包含了对s t r 函数或w c s 函数进行显式调用的代码来说,无法非常容易地同时为A N S I 和U n i c o d e 对这些代码进行编译。本章前面说过,可以创建同时为A N S I 和U n i c o d e 进行编译的单个源代码文件。若要建立双重功能,必须包含T C h a r. h 文件,而不是包含S t r i n g . h 文件。 
T C h a r. h 文件的唯一作用是帮助创建A N S I / U n i c o d e 通用源代码文件。它包含你应该用在源代码中的一组宏,而不应该直接调用s t r 函数或者w c s 函数。如果在编译源代码文件时定义了U N I C O D E ,这些宏就会引用w c s 这组函数。如果没有定义_ U N I C O D E ,那么这些宏将引用s t r这组宏。 
例如,在T C h a r. h 中有一个宏称为_ t c s c p y 。如果在包含该头文件时没有定义_ U N I C O D E ,那么_ t c s c p y 就会扩展为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 文件中定义了一个通用宏。如果使用通用宏,而不是A N S I / U n i c o d e 的特定函数名,就能够顺利地创建可以为A N S I 或U n i c o d e进行编译的源代码。 
但是,除了使用这些宏之外,还有一些操作是必须进行的。T C h a r. h 文件包含了另外一些宏.若要定义一个A N S I / U n i c o d e 通用的字符串数组,请使用下面的T C H A R 数据类型。如果定义了_ U N I C O D E ,T C H A R 将声明为下面的形式: 
typedef wchar_t TCHAR; 
如果没有定义_ U N I C O D E ,则T C H A R 将声明为下面的形式: 
typedef char TCHAR; 
使用该数据类型,可以像下面这样分配一个字符串: 
TCHAR szString[100]; 
也可以创建对字符串的指针: 
TCHAR *szError="Error"; 
不过上面这行代码存在一个问题。按照默认设置,M i c r o s o f t 公司的C + +编译器能够编译所有的字符串,就像它们是A N S I 字符串,而不是U n i c o d e 字符串。因此,如果没有定义_ U N I C O D E ,该编译器将能正确地编译这一行代码。但是,如果定义了_ U N I C O D E ,就会产生一个错误。若要生成一个U n i c o d e 字符串而不是A N S I 字符串,必须将该代码行改写为下面的样子: 
TCHAR *szError=L"Error"; 
字符串(literal string )前面的大写字母L ,用于告诉编译器该字符串应该作为U n i c o d e 字符串来编译。当编译器将字符串置于程序的数据部分中时,它在每个字符之间分散插入零字节。这种变更带来的问题是,现在只有当定义了_ U N I C O D E 时,程序才能成功地进行编译。我们需要另一个宏,以便有选择地在字符串的前面加上大写字母L 。这项工作由_ T E X T 宏来完成,_ 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 
如果没有定义_ U N I C O D E ,_ T E X T 将定义为 
#define _TEXT(x) x 
使用该宏,可以改写上面这行代码,这样,无论是否定义了_ U N I C O D E 宏,它都能够正确地进行编译。如下所示: 
TCHAR *szError=_TEXT("Error"); 
_ T E X T 宏也可以用于字符串。例如,若要检查一个字符串的第一个字符是否是大写字母J ,只需编写下面的代码即可: 
if(szError[0]==_TEXT('J')){ 
//First character is a 'J' 
... 

else{ 
//First character is not a 'J' 
... 

2.8.2 Windows定义的Unicode数据类型 
Wi n d o w s 头文件定义了表2 - 3 列出的数据类型。 
表2-3 Uincode 数据类型 
数据类型    说明 
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    指向一个恒定的U n i c o d e 字符串的指针 
这些数据类型是指U n i c o d e 字符和字符串。Wi n d o w s 头文件也定义了A N S I / U n i c o d e 通用数据类型P T S T R 和P C T S T R 。这些数据类型既可以指A N S I 字符串,也可以指U n i c o d e 字符串,这取决于当编译程序模块时是否定义了U N I C O D E 宏。 
请注意,这里的U N I C O D E 宏没有前置的下划线。_ U N I C O D E 宏用于C 运行期头文件,而U N I C O D E 宏则用于Wi n d o w s 头文件。当编译源代码模块时,通常必须同时定义这两个宏。 
2.8.3 Windows中的Unicode函数和ANSI函数 
前面已经讲过,有两个函数称为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 接受U n i c o d e 字符串,另一个C r e a t e Wi n d o w E x 接受A N S I 字符串。情况确实如此,不过,这两个函数的原型实际上是下面的样子: 
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 字符串的函数版本。函数名结尾处的大写字母W 是英文w i d e(宽)的缩写。每个U n i c o d e 字符的长度是1 6 位,因此,它们常常称为宽字符。C r e a t e Wi n d o w E x A的结尾处的大写字母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 实际上是定义为下面这种形式的一个宏: 
#ifdef UNICODE 
#define CreateWindowEx CreateWindowExW 
#else 
#define CreateWindowEx CreateWindowExA 
#endif //!UNICODE 
当编译源代码模块时,U N I C O D E 是否已经作了定义,将决定你调用的是哪个C r e a t e Wi n d o w E x 版本。当转用一个1 6 位的Wi n d o w s 应用程序时,你在编译期间可能没有定义U N I C O D E 。对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 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 源代码只不过是一个形实替换程序层或翻译层,用于分配内存,以便将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 返回时,C r e a t e Wi n d o w E x A 便释放它的内存缓存,并将窗口句柄返回给你。 
如果要创建其他软件开发人员将要使用的动态链接库(D L L ),请考虑使用下面的方法。在D L L 中提供两个输出函数。一个是A N S I 版本,另一个是U n i c o d e 版本。在A N S I 版本中,只需要分配内存,执行必要的字符串转换,并调用该函数的U n i c o d e 版本(本章后面部分介绍这个进程)。 
在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 函数的进入点,但是这些函数并不将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 版本的函数才能正确地运行。如果编译的代码调用了任何宽字符函数,应用程序将无法在Windows 98 下运行。 
Windows API 中的某些函数,比如Wi n E x e c 和O p e n F i l e 等,只是为了实现与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 函数的调用来取代对Wi n E x e c 和O p e n F i l e 函数的调用。从系统内部来讲,老的函数完全可以调用新的函数。老的函数存在的一个大问题是,它们不接受U n i c o d e 字符串。当调用这些函数时,必须传递A N S I 字符串。另一方面,所有新的和未过时的函数在Windows 2000 中都同时拥有A N S I 和U n i c o d e 两个版本。 
2.8.4 Windows字符串函数 
Wi n d o w s 还提供了一组范围很广的字符串操作函数。这些函数与C 运行期字符串函数(如s t r c p y 和w c s c p y )很相似。但是该操作系统函数是操作系统的一个组成部分,操作系统的许多组件都使用这些函数,而不使用C 运行期库。建议最好使用操作系统函数,而不要使用C 运行期字符串函数。这将有助于稍稍提高你的应用程序的运行性能,因为操作系统字符串函数常常被大型应用程序比如操作系统的外壳进程E x p l o r e r. e x e 所使用。由于这些函数使用得很多,因此,在你的应用程序运行时,它们可能已经被装入R A M 。 
若要使用这些函数,系统必须运行Windows 2000 或Windows 98 。如果安装了I n t e r n e t Explorer 4.0 或更新的版本,也可以在较早的Wi n d o w s 版本中获得这些函数。在经典的操作系统函数样式中,操作系统字符串函数名既包含大写字母,也包含小写字母,它的形式类似这个样子: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 。由于这些函数属于操作系统函数,因此,当创建应用程序时,如果定义了U N I C O D E (不带前置下划线),那么它们的符号将扩展为宽字符版本。 

2.9 成为符合ANSI和Unicode的应用程序 
即使你不打算立即使用U n i c o d e ,最好也应该着手将你的应用程序转换成符合U n i c o d e 的应用程序。下面是应该遵循的一些基本原则: 
• 将文本串视为字符数组,而不是c h a r s 数组或字节数组。 
• 将通用数据类型(如T C H A R 和P T S T R )用于文本字符和字符串。 
• 将显式数据类型(如B Y T E 和P B Y T E )用于字节、字节指针和数据缓存。 
• 将T E X T 宏用于原义字符和字符串。 
• 执行全局性替换(例如用P T S T R 替换P S T R )。 
• 修改字符串运算问题。例如函数通常希望你在字符中传递一个缓存的大小,而不是字节。 
这意味着你不应该传递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 )。另外,如果需要为字符串分配一个内存块,并且拥有该字符串中的字符数目,那么请记住要按字节来分配内存。这就是说,应该调用malloc(nCharacters *sizeof(TCHAR)),而不是调用m a l l o c( n C h a r a c t e r s )。在上面所说的所有原则中,这是最难记住的一条原则,如果操作错误,编译器将不发出任何警告。 
当我为本书的第一版编写示例程序时,我编写的原始程序只能编译为A N S I 程序。后来,当我开始撰写本章的内容时,我想我应该鼓励使用U n i c o d e ,并且打算创建一些示例程序,以便展示你可以非常容易地编写既可以用U n i c o d e 也可以用A N S I 来编译的程序。这时我发现最好的办法是将本书的所有示例程序进行转换,使它们都能够用U n i c o d e 和A N S I 进行编译。 
我用了大约4 个小时将所有程序进行了转换。考虑到我以前从来没有这方面的转换经验,这个速度是相当不错了。 
2.9.1 Windows字符串函数 
Wi n d o w s 也提供了一组用于对U n i c o d e 字符串进行操作的函数,表2 - 4 对它们进行了描述。 
表2-4 对U n i c o d e 字符串进行操作的函数 
函数    描述 
l s t r c a t    将一个字符串置于另一个字符串的结尾处 
l s t r c m p    对两个字符串进行区分大小写的比较 
l s t r c m p i    对两个字符串进行不区分大小写的比较 
l s t r c p y    将一个字符串拷贝到内存中的另一个位置 
l s t r l e n    返回字符串的长度(按字符数来计量) 
这些函数是作为宏来实现的,这些宏既可以调用函数的U n i c o d e 版本,也可以调用函数的A N S I 版本,这要根据编译源代码模块时是否已经定义了U N I C O D E 而定。例如,如果没有定义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 。 
有两个字符串函数,即l s t r c m p 和l s t r c m p i ,它们的行为特性与等价的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 只是对字符串中的代码点的值进行比较,这就是说,这些函数将忽略实际字符的含义,只是将第一个字符串中的每个字符的数值与第二个字符串中的字符的数值进行比较。而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 的调用来实现的。 
int CompareString( 
LCID lcid, 
DWORD fdwStyle, 
PCWSTR pString1, 
int cch1, 
PCTSTR pString2, 
int cch2); 
该函数对两个U n i c o d e 字符串进行比较。C o m p a r e S t r i n g 的第一个参数用于设定语言I D(L C I D ),这是个3 2 位值,用于标识一种特定的语言。C o m p a r e S t r i n g 使用这个L C I D 来比较这两个字符串,方法是对照一种特定的语言来查看它们的字符的含义。这种操作方法比C 运行期函数简单地进行数值比较更有意义。 
当l s t r c m p 函数系列中的任何一个函数调用C o m p a r e S t r i n g 时,该函数便将调用Wi n d o w s 的G e t T h r e a d S t r i n g 函数的结果作为第一个参数来传递: 
LCID GetThreadLocale(); 
每次创建一个线程时,它就被赋予一种语言。函数将返回该线程的当前语言设置。 
C o m p a r e S t r i n g 的第二个参数用于标识一些标志,这些标志用来修改该函数比较两个字符串时所用的方法。表2 - 5 显示了可以使用的标志。 
表2-5 CompareString 的标志及含义 
标志    含义 
N O R M _ I G N O R E C A S E    忽略字母的大小写 
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    忽略无间隔字符 
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    不区分单字节字符与作为双字节字符的同一个字符 
S O RT _ S T R I N G S O RT    将标点符号作为普通符号来处理 
当l s t r c m p 调用C o m p a r e S t r i n g 时,它传递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 时,它就传递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 个参数用于设定两个字符串和它们各自的长度。如果为c c h 1 参数传递- 1 ,那么该函数将认为p S t r i n g 1 字符串是以0结尾,并计算该字符串的长度。对于p S t r i n g 2 字符串来说,参数c c h 2 的作用也是一样。 
其他C 运行期函数没有为U n i c o d e 字符串的操作提供很好的支持。例如,t o l o w e r 和t o u p p e r函数无法正确地转换带有重音符号的字符。为了弥补C 运行期库中的这些不足,必须调用下面这些Wi n d o w s 函数,以便转换U n i c o d e 字符串的大小写字母。这些函数也可以正确地用于A N S I字符串。 
头两个函数: 
PTSTR CharLower(PTSTR pszString); 
PTSTR CharUpper(PTSTR pszString); 
既可以转换单个字符,也可以转换以0 结尾的整个字符串。若要转换整个字符串,只需要传递字符串的地址即可。若要转换单个字符,必须像下面这样传递各个字符: 
将单个字符转换成一个P T S T R ,便可调用该函数,将一个值传递给它,在这个值中,较低的1 6 位包含了该字符,较高的1 6 位包含0 。当该函数看到较高位是0 时,该函数就知道你想要转换单个字符,而不是整个字符串。返回的值是个3 2 位值,较低的1 6 位中是已经转换的字符。 
TCHAR cLowerCase=CharLower((PTSTR szString[0]); 
将单个字符转换成一个P T S T R ,便可调用该函数,将一个值传递给它,在这个值中,较低的1 6 位包含了该字符,较高的1 6 位包含0 。当该函数看到较高位是0 时,该函数就知道你想要转换单个字符,而不是整个字符串。返回的值是个3 2 位值,较低的1 6 位中是已经转换的字符。 
下面两个函数与前面两个函数很相似,差别在于它们用于转换缓存中包含的字符(该缓存不必以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 ,返回一个值,指明某个字符是字母字符、小写字母还是大写字母。Windows API 提供了一些函数,也能返回这些信息,但是Wi n d o w s 函数也要考虑用户在控制面板中指定的语言: 
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 字符和字符串。但是,如果在没有定义_ 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 公司已经给C 运行期的p r i n t f 函数家族增加了一些特殊的域类型。其中有些域类型尚未被ANSI C 采用。新类型使你能够很容易地对A N S I 和U n i c o d e 字符和字符串进行混合和匹配。操作系统的w s p r i n t f 函数也得到了增强。下面是一些例子(请注意大写S 和小写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 资源 
当资源编译器对你的所有资源进行编译时,输出文件是资源的二进制文件。资源(字符串表、对话框模板和菜单等)中的字符串值总是写作U n i c o d e 字符串。在Windows 98 和Wi n d o w s 2 0 0 0 下,如果应用程序没有定义U N I C O D E 宏,那么系统就会进行内部转换。 
例如,如果在编译源代码模块时没有定义U N I C O D E ,调用L o a d S t r i n g 实际上就是调用L o a d S t r i n g A 函数。这时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 文本文件仍然非常少。实际上,M i c r o s o f t 公司自己的大多数产品并没有配备任何U n i c o d e 文本文件。但是预计将来这种情况是会改变的(尽管这需要一个很长的过程)。当然,Windows 2000 的N o t e p a d (记事本)应用程序允许你既能打开U n i c o d e 文件,也能打开A N S I 文件,并且可以创建这些文件。图2 - 1 显示了N o t e p a d 的Save As (文件另存为)对话框。请注意可以用不同的方法来保存文本文件。 

图2-1 Windows 2000 Notepad 的File Save As 对话框 
对于许多用来打开文本文件和处理这些文件的应用程序(如编译器)来说,打开一个文件后,应用程序就能方便地确定该文本文件是包含A N S I 字符还是U n i c o d e 字符。I s Te x t U n i c o d e 函数能够帮助进行这种区分: 
DWORD IsTextUnicode(CONST PVOID pvBuffer, int cb,PINT pResult); 
文本文件存在的问题是,它们的内容没有严格和明确的规则,因此很难确定该文件是包含A N S I 字符还是U n i c o d e 字符。I s Te x t U n i c o d e 使用一系列统计方法和定性方法,以便猜测缓存的内容。由于这不是一种确切的科学方法,因此I s Te x t U n i c o d e 有可能返回不正确的结果。 
第一个参数p v B u ff e r 用于标识要测试的缓存的地址。该数据是个无效指针,因为你不知道你拥有的是A N S I 字符数组还是U n i c o d e 
字符数组。 
第二个参数c b 用于设定p v B u ff e r 指向的字节数。同样,由于你不知道缓存中放的是什么,因此c b 是个字节数,而不是字符数。请注意,不必设定缓存的整个长度。当然,I s Te x t U n i c o d e能够测试的字节越多,得到的结果越准确。 
第三个参数p R e s u l t 是个整数的地址,必须在调用I s Te x t U n i c o d e 之前对它进行初始化。对该整数进行初始化后,就可以指明你要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 将执行它能够进行的所有测试(详细说明请参见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 。确实是这样,尽管M i c r o s o f t将该函数的原型规定为返回D W O R D ,但是它实际上返回一个布尔值。如果在p R e s u l t 参数指向的整数中必须进行特定的测试,该函数就会在返回之前设定整数中的信息位,以反映每个测试的结果。 
Wi n d o w s 9 8 在Windows 98 下,I s Te x t U n i c o d e 函数没有有用的实现代码,它只是返回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 参数用于标识一个与多字节字符串相关的代码页号。d w F l a g s 参数用于设定另一个控件,它可以用重音符号之类的区分标记来影响字符。这些标志通常并不使用,在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 参数用于指明该字符串的长度(按字节计算)。如果为c c h M u l t i B y t e 参数传递- 1 ,那么该函数用于确定源字符串的长度。 
转换后产生的U n i c o d e 版本字符串将被写入内存中的缓存,其地址由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 ,那么该参数将不执行字符串的转换,而是返回为使转换取得成功所需要的缓存的值。一般来说,可以通过下列步骤将多字节字符串转换成U n i c o d e 等价字符串: 
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 参数来传递,并传递第一次调用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 参数。 
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 将宽字符串转换成等价的多字节字符串,如下所示: 
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 参数用于标识与新转换的字符串相关的代码页。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 参数用于指明该字符串的长度(用字符数来计量)。如果你为c c h Wi d e C h a r 参数传递- 1 ,那么该函数用于确定源字符串的长度。 
转换产生的多字节版本的字符串被写入由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参数中设定该缓存的最大值(用字节来计量)。如果传递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 参数,那么该函数将返回目标缓存需要的大小值。通常可以使用将多字节字符串转换成宽字节字符串时介绍的一系列类似的事件,将宽字节字符串转换成多字节字符串。 
你会发现,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 个,即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 函数遇到一个宽字节字符,而该字符在u C o d e P a g e 参数标识的代码页中并没有它的表示法时,Wi d e C h a r To M u l t i B y t e 函数才使用这两个参数。如果宽字节字符不能被转换,该函数便使用p D e f a u l t C h a r 参数指向的字符。如果该参数是N U L L (这是大多数情况下的参数值),那么该函数使用系统的默认字符。该默认字符通常是个问号。这对于文件名来说是危险的,因为问号是个通配符。 
p f U s e d D e f a u l t C h a r 参数指向一个布尔变量,如果宽字符串中至少有一个字符不能转换成等价多字节字符,那么函数就将该变量置为T R U E 。如果所有字符均被成功地转换,那么该函数就将该变量置为FA L S E 。当函数返回以便检查宽字节字符串是否被成功地转换后,可以测试该变量。同样,通常为该测试传递N U L L 。 
关于如何使用这些函数的详细说明,请参见Platform SDK 文档。 
如果使用这两个函数,就可以很容易创建这些函数的U n i c o d e 版本和A N S I 版本。例如,你可能有一个动态链接库,它包含一个函数,能够转换字符串中的所有字符。可以像下面这样编写该函数的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); 

你可以编写该函数的A N S I 版本以便该函数根本不执行转换字符串的实际操作。你也可以编写该函数的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 字符串。该函数类似下面的样子: 
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), 

最后,在用动态链接库分配的头文件中,可以像下面这样建立这两个函数的原型: 
BOOL StringReverseW (PWSTR pWideCharStr); 
BOOL StringReverseA (PSTR pMultiByteStr); 

#ifdef UNICODE 
#define StnngReverse StringReverseW 
#else 
#define StringRevcrsc StringReverseA 
#endif // UNICODE 
第3章 内核对象 
在介绍Windows API 的时候,首先要讲述内核对象以及它们的句柄。本章将要介绍一些比较抽象的概念,在此并不讨论某个特定内核对象的特性,相 反只是介绍适用于所有内核对象的特性。
首先介绍一个比较具体的问题,准确地理解内核对象对于想要成为一名Wi n d o w s 软件开发能手的人来说是至关重要的。内核对象可以供系统和 应用程序使用来管理各种各样的资源,比如进程、线程和文件等。本章讲述的概念也会出现在本书的其他各章之中。但是,在你开始使用实际的函数 来操作内核对象之前,是无法深刻理解本章讲述的部分内容的。因此当阅读本书的其他章节时,可能需要经常回过来参考本章的内容。 

3.1 什么是内核对象 
作为一个Wi n d o w s 软件开发人员,你经常需要创建、打开和操作各种内核对象。系统要创建和操作若干类型的内核对象,比如存取符号对象、 事件对象、文件对象、文件映射对象、I / O 完成端口对象、作业对象、信箱对象、互斥对象、管道对象、进程对象、信标对象、线程对象和等待计 时器对象等。这些对象都是通过调用函数来创建的。例如,C r e a t e F i l e M a p p i n g 函数可使系统能够创建一个文件映射对象。每个内 核对象只是内核分配的一个内存块,并且只能由该内核访问。该内存块是一种数据结构,它的成员负责维护该对象的各种信息。有些数据成员(如安全性描述符、使用计数等)在所有对象类型中是相同的,但大多数数据成员属于特定的对象类型。例如,进程对象有一个进程I D 、一个基 本优先级和一个退出代码,而文件对象则拥有一个字节位移、一个共享模式和一个打开模式。 
由于内核对象的数据结构只能被内核访问,因此应用程序无法在内存中找到这些数据结构并直接改变它们的内容。M i c r o s o f t 规定了这个限 制条件,目的是为了确保内核对象结构保持状态的一致。这个限制也使M i c r o s o f t 能够在不破坏任何应用程序的情况下在这些结构中添加、 删除和修改数据成员。 
如果我们不能直接改变这些数据结构,那么我们的应用程序如何才能操作这些内核对象呢?解决办法是,Wi n d o w s 提供了一组函数,以便用定 义得很好的方法来对这些结构进行操作。这些内核对象始终都可以通过这些函数进行访问。当调用一个用于创建内核对象的函数时,该函数就返回一 个用于标识该对象的句柄。该句柄可以被视为一个不透明值,你的进程中的任何线程都可以使用这个值。将这个句柄传递给Wi n d o w s 的各个函 数,这样,系统就能知道你想操作哪个内核对象。本章后面还要详细讲述这些句柄的特性。 
为了使操作系统变得更加健壮,这些句柄值是与进程密切相关的。因此,如果将该句柄值传递给另一个进程中的一个线程(使用某种形式的进程间的 通信)那么这另一个进程使用你的进程的句柄值所作的调用就会失败。在3 . 3 节“跨越进程边界共享内核对象”中,将要介绍3 种机制,使多个进 程能够成功地共享单个内核对象。 
3.1.1 内核对象的使用计数 
内核对象由内核所拥有,而不是由进程所拥有。换句话说,如果你的进程调用了一个创建内核对象的函数,然后你的进程终止运行,那么内核对象不 一定被撤消。在大多数情况下,对象将被撤消,但是如果另一个进程正在使用你的进程创建的内核对象,那么该内核知道,在另一个进程停止使用该 对象前不要撤消该对象,必须记住的是,内核对象的存在时间可以比创建该对象的进程长。 
内核知道有多少进程正在使用某个内核对象,因为每个对象包含一个使用计数。使用计数是所有内核对象类型常用的数据成员之一。当一个对象刚刚 创建时,它的使用计数被置为1 。然后,当另一个进程访问一个现有的内核对象时,使用计数就递增1 。当进程终止运行时,内核就自动确定该进程 仍然打开的所有内核对象的使用计数。如果内核对象的使用计数降为0 ,内核就撤消该对象。这样可以确保在没有进程引用该对象时系统中不保留任 何内核对象。 
3.1.2 安全性 
内核对象能够得到安全描述符的保护。安全描述符用于描述谁创建了该对象,谁能够访问或使用该对象,谁无权访问该对象。安全描述符通常在编写 服务器应用程序时使用,如果你编写客户机端的应用程序,那么可以忽略内核对象的这个特性。 
Windows 98 根据原来的设计,Windows 98 并不用作服务器端的操作系统。为此,M i c r o s o f t 公司没有在Windows 98 中配备安全特性。不 过,如果你现在为Windows 98设计软件,在实现你的应用程序时仍然应该了解有关的安全问题,并且使用相应的访问信息,以确保它能在Windows 2000上正确地运行. 
用于创建内核对象的函数几乎都有一个指向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 ,这样就可以创建带有默认安全性的内核对象。默认安全性意味着对象的管理小组的任何成员和对象的创 建者都拥有对该对象的全部访问权,而其他所有人均无权访问该对象。但是,可以指定一个S E C U R I T Y _ AT T R I B U T E S 结构,对它进 行初始化,并为该参数传递该结构的地址。S E C U R I T Y _ AT T R I B U T E S 结构类似下面的样子:尽管该结构称为S E C U R I T Y _ AT T R I B U T E S ,但是它包含的与安全性有关的成员实际上只有一个,即l p S e c u r i t y D e s c r i p t o r 。如果你想要限制人们对你 创建的内核对象的访问,必须创建一个安全性描述符,然后像下面这样对S E C U R I T Y _ AT T R I B U T E S 结构进行初始化: 
typedef struct _SECURITY_ATTRIBUTES 

DWORD nLength, 
LPVOID lpSecurityDescriptor; 
BOOL bInherttHandle; 
} SECURITY_ATTRIBUTES; 
尽管该结构称为S E C U R I T Y _ AT T R I B U T E S ,但是它包含的与安全性有关的成员实际上只有一个,即l p S e c u r i t y D e s c r i p t o r 。如果你想要限制人们对你创建的内核对象的访问,必须创建一个安全性描述符,然后像下面这样对S E C U R I T Y _ AT T R I B U T E S 结构进行初始化: 
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 这个成员与安全性毫无关系,因此准备推迟到本章后面部分继承性一节中再介绍b I n h e r i t H a n d l e 这个成员。 
当你想要获得对相应的一个内核对象的访问权(而不是创建一个新对象)时,必须设定要对该对象执行什么操作。例如,如果想要访问一个现有的文 件映射内核对象,以便读取它的数据,那么应该调用下面这个O p e n f i l e M a p p i n g 函数: 
HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ, 
FALSE, "MyFileMapping"); 
通过将F I L E _ M A P _ R E A D 作为第一个参数传递给O p e n F i l e M a p p i n g ,指明打算在获得对该文件映象的访问权后读取该文件 ,O p e n F i l e M a p p i n g 函数在返回一个有效的句柄值之前,首先执行一次安全检查。如果(已登录用户)被允许访问现有的文件映射内 核对象,O p e n F i l eM a p p i n g 就返回一个有效的句柄。但是,如果被拒绝访问该对象,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 ),同样,大多数应用程序并不使用该安全性,因此 将不进一步讨论这个问题。 
Windows 98 虽然许多应用程序不需要考虑安全性问题,但是Wi n d o w s 的许多函数要求传递必要的安全访问信息。为Windows 98 设计的若干应 用程序在Windows 2000 上无法正确地运行,因为在实现这些应用程序时没有对安全问题给于足够的考虑。 
例如,假设一个应用程序在开始运行时要从注册表的子关键字中读取一些数据。为了正确地进行这项操作,你的代码应该调用R e g O p e n K e y E x ,传递K E Y_Q U E RY_VA L U E ,以便获得必要的访问权。 
但是,许多应用程序原先是为Windows 98 开发的,当时没有考虑到运行Wi n d o w s2 0 0 0 的需要。由于Windows 98 没有解决注册表的安全问题 ,因此软件开发人员常常要调用R e g O p e n K e y E x 函数,传递K E Y _ A l l _ A C C E S S ,作为必要的访问权。开发人员这样做的原因 是,它是一种比较简单的解决方案,意味着开发人员不必考虑究竟需要什么访问权。问题是注册表的子关键字可以被用户读取,但是不能写入。 
因此,当该应用程序现在放在Windows 2000 上运行时,用K E Y _ A L L _ A C C E S S 调用R e g O p e n K e y E x 就会失败,而且,没有相 应的错误检查方法,应用程序的运行就会产生不可预料的结果。 
如果开发人员想到安全问题,把K E Y _ A L L _ A C C E S S 改为K E Y _ Q U E RY _ VA L U E ,则该产品可适用于两种操作系统平台。 
开发人员的最大错误之一就是忽略安全访问标志。使用正确的标志会使最初为Windows 98 设计的应用程序更易于向Windows 2000 转换。 
除了内核对象外,你的应用程序也可以使用其他类型的对象,如菜单、窗口、鼠标光标、刷子和字体等。这些对象属于用户对象或图形设备接口(G D I )对象,而不是内核对象。当初次着手为Wi n d o w s 编程时,如果想要将用户对象或G D I 对象与内核对象区分开来,你一定会感到不知所 措。比如,图标究竟是用户对象还是内核对象呢?若要确定一个对象是否属于内核对象,最容易的方法是观察创建该对象所用的函数。创建内核对象 的所有函数几乎都有一个参数,你可以用来设定安全属性的信息,这与前面讲到的C r e a t e F i l e M a p p i n g 函数是相同的。 
用于创建用户对象或G D I 对象的函数都没有P S E C U R I T Y _ AT T R I B U T E S 参数。例如,让我们来看一看下面这个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 进程的内核对象句柄表 
当一个进程被初始化时,系统要为它分配一个句柄表。该句柄表只用于内核对象,不用于用户对象或G D I 对象。句柄表的详细结构和管理方法并没有 具体的资料说明。通常我并不介绍操作系统中没有文档资料的那些部分。不过,在这种情况下,我会进行例外处理,因为,作为一个称职的Wi n d o w s 程序员,必须懂得如何管理进程的句柄表。由于这些信息没有文档资料,因此不能保证所有的详细信息都正确无误,同时,在Windows 2000 Windows 98 和Windows CE 中,它们的实现方法是不同的。为此,请认真阅读下面介绍的内容以加深理解,在此不学习系统是如何进行操作的。 
表3 -
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
U n i c o d e与Windows编程(1) | 梦之城堡
VB常数与函数(三)
Microsoft Windows CE 编程的十点忠告
Win32控制台程序是什么
DLL中的共享内存(实例)
VC知识库文章 - C++字符串完全指引之一
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服