由于没有办法将所有的设定放在CmnHdr.h中,所以我在Project Settings对话方块中对每个范例应用程序之专案设定做了一些修改:
- 在General页签中,设定了Output Files directory的内容,以使所有的 .exe与 .dll文件皆存放在单一的目录中。
- 在C/C++ 页签中,我在Category清单中选择了Code Generation category选项,并在Use Run-Time Library栏位中选择了Multithreaded DLL选项。
这些就是我唯一明确地修改设定的地方;其他部份则接受预设的设定。请注意,每个专案的debug与release模式做了上述二个修改。我在原始程序代码文件中设定了所有其他之编译器与连结器的设定,所以您要在专案中使用此原始码模组时,这些设定就会产生作用。
CmnHdr.h标头文件
所有范例程序皆在引入其他标头档前将CmnHdr.h标头档引入,CmnHdr.h标头文件的内容放在此附录后的列表A-1中,以使工作变得轻松一点。该文件包含了巨集码、连结器的指示词以及其他贯穿所有范例应用程序的常见程序代码。当想尝试某些不同的东西时,需要做的事就是修改CmnHdr.h,然后重新建置所有范例应用程序。CmnHdr.h放置在随书光碟的根目录中。
本附录剩馀的部份即是对CmnHdr.h标头文件的每个部份进行讨论,除了解释每部份的基本原理外,还描述该如何做以及为什么需要在重新建置范例应用程序前做这些修改的内容。
迫使连结器寻找一个(w)WinMain进入点函数
在读者将此原始程序模组加到一个新的Microsoft Visual C++ 专案并尝试建置该专案时,会接收到连结器的错误讯息。这个问题是因为他们建立了一个Win32 Console Application专案,所以导致连结器会去寻找一个(w)main进入点函数。由于本书的范例应用程序皆为GUI应用程序,所以我的原始程序代码中皆以_tWinMain进入点函数取代,因此才会使连结器发出抱怨的讯息。
针对这个问题的标准回应如下:删除该专案、使用Visual C++ 建立一个新的Win32 Application专案(请注意「Console」不该出现在此专案类别中),然后将此原始码文件加入。连结器会寻找一个(w)WinMain进入点函数,在此程序代码中已经提供此点,而得以适当地建置专案。
为了减少将会因为此议题而收到的e-mail数量,我新增了一个pragma至CmnHdr.h中,以便您用Visual C++ 建立了一个Win32 Console Application专案时,可以强迫连结器寻找(w)WinMain进入点函数。
在《Programming Applications for Microsoft Windows, Fourth Edition》(Jeffrey Richter, Microsoft Press, 1999)一书的第四章中,详细讨论了Visual C++ 之专案类型的相关内容、如何选择连结器应该寻找的进入点函数,以及如何覆写连结器的预设行为设定。
Windows版本之建置选项
因为某些范例应用程序呼叫了Microsoft Windows 2000中的新增函数,CmnHdr.h将此部份定义为 _WIN32_WINNT符号,其内容如下:
#define _WIN32_WINNT 0x0500
因为新的Windows 2000函数原型放在Windows标头文件中,所以必须定义此符号,其内容如下:
#if (_WIN32_WINNT >= 0x0500) WINBASEAPI BOOL WINAPI AssignProcessToJobObject( IN HANDLE hJob, IN HANDLE hProcess ); #endif /*_WIN32_WINNT >= 0x0500 */
除非您像我一样特别定义 _WIN32_WINNT符号(在引入Windows.h前),否则新函数的原型不会被宣告,而且当您试图呼叫这些函数时,编译器将会产生错误讯息。Microsoft用 _WIN32_WINNT符号来保护这些函数,以帮助确认您所开发之可在多种Microsoft Windows NT与Microsoft Windows 98版本中执行的应用程序。
Unicode建置选项
大部份的范例应用程序皆可以被编译为ANSI或Unicode的型式。本书的主题为Windows 2000之服务编写,所以预设为Unicode的型式,因为它允许应用程序使用较少的内存并可更快速执行。然而,如果您想要在自己的ANSI应用程序中使这些程序代码也可以正常执行。
说明
在时间上,Microsoft在作业系统中已经新增了越来越多只支援Unicode的特色。例如,在第九章中讨论之管理受信任帐户的Net函数就只支援Unicode字串。因为这些函数出现在程序代码中,所以本书中的某些范例应用程序会编译成只支援Unicode的型式。请参阅《Programming Applications for Microsoft Windows, Fourth Edition》的第二章内容,以取得更多Unicode的相关资讯。
Windows定义与Warning Level 4
当我在开发软件时,总是试着确认程序代码在经过编译后没有出现错误与警告讯息。我也喜欢在较高的警告层级下编译程序,所以编译器会为做了大部份的工作,甚至检查程序代码的最小细节。在Microsoft C/C++ 编译器中,我使用Warning Level 4来建置所有的范例应用程序。
在CmnHdr.h的这个部份中,我确认它的Warning Level设定为4,而CmnHdr.h中引入了标准的Windows.h标头文件。在Warning level 4上,编译器会发出不需考虑问题的「warnings」事件,所以我明确地告诉编译器必须忽略某些使用#pragma warning指示词之良性警告事件。
pragma讯息协助程序巨集
当我在编写程序代码时,通常喜欢立即地执行某些工作,然后再将它修改得更完美。为了提醒自己必须修改某些程序代码,我加入了一行像这样的程序代码:
#pragma message("Fix this later")
当编译器编译此行时,它会输出一个字串以提醒我必须做某些事;然而,这个讯息并不是很有帮助。所以我决定找出一个方法使得编译器输出原始程序代码文件的名称以及其中出现pragma的行号。有了这个资讯,不仅可以知道有某些事必须做,还可以立即了解应修改之程序代码的位置。
为了输出原始程序代码文件以及行号,您必须有技巧的使用一系列pragma message指示词之巨集程序代码。我将产生的巨集命名为chMSG,使用的方法就像这样:
#pragma chMSG(Fix this later)
当此行被编译后,编译器会产生一行如下所示的讯息:
C:/CD/CmnHdr.h(82):Fix this later
使用Microsoft Visual Studio,您可以在输出视窗双击此行,它会自动地移到对应的程序代码上。
chMSG巨集不需要在文字字串前后使用引号。
chINRANGE、chDIMOF以及chSIZEOFSTRING巨集
在应用程序中,频繁地使用了便于使用的chINRANGE、chDIMOF以及chSIZEOFSTRING巨集。第一个巨集为chINRANGE,检查一个值是否存在两个其他值之间;第二个巨集为chDIMOF,简单地回传阵列中的项目数,首先用sizeof运算子以位元组为单位,计算整个阵列的尺寸,然后再用阵列中之单一项目所需的位元组数字来划分此数目;第叁个巨集为chSIZEOFSTRING,回传一个被零终止字串占用的大小。
chROUNDDOWN与chROUNDUP之内嵌范本函数(Template Functions)
chROUNDDOWN与chROUNDUP之内嵌范本函数只从下或从上循环一个值至最接近的特定倍数。
chBEGINTHREADEX巨集
本书中所有的多线程范例皆使用了_beginthreadex函数,该函数包含在Microsoft之C/C++ 执行时期程序库中,以取代作业系统的CreateThread函数。使用_beginthreadex的原因在于它准备了新的线程,以使用C/C++ 执行时期程序库函数,以及在线程返回时,确认经过线程的C/C++ 执行时期程序库资讯已被删除(请参阅《Programming Applications for Microsoft Windows, Fourth Edition》一书的第六章内容,以取得更多详细的资讯)。_beginthreadex函数的原型定义如下:
unsigned long __cdecl _beginthreadex( void *, unsigned, unsigned (__stdcall *)(void *), void *, unsigned, unsigned *);
虽然_beginthreadex函数的参数与CreateThread函数相同,然而其参数的资料型别并不相符。以下是CreateThread函数的原型定义:
typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(PVOID pvParam); HANDLE CreateThread( PSECURITY_ATTRIBUTES psa, DWORD cbStack, PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD fdwCreate, PDWORD pdwThreadId);
Microsoft没有使用Windows资料型别来建立_beginthreadex函数的原型,因为Microsoft的C/C++ 执行时期小组不想使用到作业系统上的任何从属关系。我赞同这个决定,然而,这也让_beginthreadex函数的使用变得更困难。
实际上,Microsoft定义_beginthreadex函数原型时产生了二个问题。首先,用于功能的某些资料类型不和经由CreateThread功能使用之原始型别相匹配。例如,Windows之DWORD资料型别定义如下:
typedef unsigned long DWORD;
不但将此资料类型用在CreateThread的cbStack参数,也用于它的fdwCreate参数。问题是_beginthreadex的这二个参数定义为unsigned型别,实际上为unsigned in型别。编译器认为unsigned int与unsigned long不同,因此产生了一个警告讯息。因为_beginthreadex函数不是标准的C/C++ 执行时期程序库,并且只有在选择呼叫CreateThread函数时才存在,我相信Microsoft应该使用此_beginthreadex原型的方法,以避免产生警告讯息:
unsigned long __cdecl _beginthreadex( void *psa, unsigned long cbStack, unsigned (__stdcall *) (void *pvParam), void *pvParam, unsigned long fdwCreate, unsigned long *pdwThreadId);
第二个问题只是第一个问题的小变化而已。_beginthreadex函数回传了一个unsigned long值,表示对新建立之线程的handle。一个应用程序通常会在一个HANDLE型别之资料变数中储存此回传值,如下所示:
HANDLE hThread = _beginthreadex(...);
此程序代码会导致编译器产生一个警告讯息。要避免编译器产生警告讯息,您必须重新编写该行程序,以下展示一个范例:
HANDLE hThread = (HANDLE) _beginthreadex(...);
虽然如此,但是它非常不方便。为了使工作变得容易些,可在CmnHdr.h中定义了一个chBEGINTHREADEX巨集,让该巨集执行所有此类型的工作:
typedef unsigned (__stdcall *PTHREAD_START) (void *); #define chBEGINTHREADEX(psa, cbStack, pfnStartAddr, / pvParam, fdwCreate, pdwThreadId) / ((HANDLE)_beginthreadex( / (void *) (psa), / (unsigned) (cbStack), / (PTHREAD_START) (pfnStartAddr), / (void *) (pvParam), / (unsigned) (fdwCreate), / (unsigned *) (pdwThreadId)))
改善x86平台的DebugBreak函数
有时候当处理程序不在侦错工具下执行时,会想在程序代码中放置一个中断点,可以在Windows中经由一个线程呼叫DebugBreak函数来完成。此函数存在Kernel32.dll中,可让您指派一个侦错工具至处理程序。一旦侦错工具被指派后,指令指标会被放置在CPU指令集上,以产生一个中断点。此指令被Kernel32.dll内的DebugBreak函数所控制,所以要察看程序代码就必须跳出DebugBreak函数。
在x86架构上,您会经由执行一个「int 3」的CPU指令来执行一个中断点,所以我重新将DebugBreak定义成此内嵌组合语言的指令。当我的DebugBreak巨集执行时,我不会呼叫至Kernel32.dll;中断点会正确地在我的程序代码中产生,而指令指标会被适当的放在下一个C/C++ 语言叙述上。这个经过改进的DebugBreak巨集正好使事情变得更方便。
建立软件例外程序代码
当您要使用软件例外时,必须建立您拥有的32-bit例外程序代码。这些程序代码采用了一个特定的格式(在《Programming Applications for Microsoft Windows, Fourth Edition》一书之第二十四章中讨论)。为了使建立这些程序代码的工作变得较为容易,可以利用MAKESOFTWAREEXCEPTION巨集。
chMB巨集
chMB巨集只显示一个讯息方块,标题为呼叫处理程序之执行档的完整路径。
chASSERT和chVERIFY巨集
为了寻找范例应用程序的潜在问题,我在程序代码的各处放置了chASSERT巨集。chASSERT巨集会测试哪个运算式被为TRUE之x所确认,反之,则显示一个讯息方块指示该文件、行号以及执行失败的运算式。在应用程序的Release建置中,此巨集不会使执行档的大小膨胀。除了运算式在Release以及Debug建置中皆会被评估外,chVERIFY巨集几乎与chASSERT巨集相同。
chHANDLE_DLGMSG巨集
当您在使用讯息萃取器(Message Cracker)对话方块时,不该从Microsoft之WindowsX.h标头档中使用HANDLE_MSG巨集,因为它并不回传TRUE或FALSE值以指示讯息是否已被对话方块程序处理。我的chHANDLE_DLGMSG巨集显示了视窗讯息的回传值并适当地处理使用于对话方块中的程序。
chSETDLGICONS巨集
因为多数的范例应用程序皆使用一个对话方块为它们的主视窗,您必须手动地修改对话方块图示以使它正确地显示在工作列上、工作转换视窗以及应用程序标题中。chSETDLGICONS巨集通常在对话方块接收一个WM_INITDIALOG讯息时被呼叫,以使图示被正确地设定。
确定主机系统支援Unicode
CmnHdr.h文件包含一个在《Programming Applications for Microsoft Windows, Fourth Edition》一书中使用的Unicode检查巨集,以确定主机系统支援Unicode。由于本书的范例应用程序需要在Windows 2000中执行,所以不需要此巨集。
OS版本检查之内嵌函数
CmnHdr.h文件包含二个在《Programming Applications for Microsoft Windows, Fourth Edition》一书中使用的内嵌函数,以检查主机系统的作业系统版本。由于本书的范例应用程序需要在Windows 2000中执行,所以不需要这些函数。
CsystemInfo之C++ 类别
这是一个非常简单的C++ 类别,它包装了Windows的SYSTEM_INFO结构,并在一个类别实例被建构后初始此结构。许多范例应用程序皆在此结构中使用此成员;在一个C/C++ 类别中包装该架构,并确保这些成员已被初始化以及一些程序代码的简化。
CmnHdr.h /******************************************************************** 模组:CmnHdr.h 通告:Copyright (c)2000 Jeffrey Richter 目的:Common标头文件,包含了便于使用之巨集以及本书所有应用程序使用的定义。 请参阅附录A。 ********************************************************************/ #pragma once // 每个编辑单元皆引入此标头档一次 ///执行Windows子系统/// #pragma comment(linker, "/subsystem:Windows,5") ///执行Windows 2000 #pragma comment(linker, "/version:5") Windows版本建构选项/ #define _WIN32_WINNT 0x0500 #define WINVER 0x0500 Unicode建构选项/ // 若不在x86之CPU下编译,则使用Unicode编译 #ifndef _M_IX86 #define UNICODE #endif // 若不使用Unicode编译,请将以下这行做注解 #define UNICODE // 当使用了Unicode Windows函数,也使用了Unicode C-Runtime函数 #ifdef UNICODE #define _UNICODE #endif /包含Windows定义/ #pragma warning(push, 4) #include <Windows.h> #include <TChar.h> /核对适当的标头档// #ifndef WT_EXECUTEINPERSISTENTIOTHREAD #pragma message("You are not using the latest Platform SDK header/library ") #pragma message("files.This may prevent the project from building correctly.") #pragma message("You may install the Platform SDK from the book's CD-ROM or ") #pragma message("from http://msdn.microsoft.com/downloads/") #endif //允许程序代码在warning level 4中编译 /// /* 曾被使用之非标准延伸「单一注解」 **/ #pragma warning(disable:4001) // 未被参考的正式参数 #pragma warning(disable:4100) // 注意:建立先行编译标头 #pragma warning(disable:4699) // 非内嵌的函数 #pragma warning(disable:4710) // 已被移除之未被参考的内嵌函数 #pragma warning(disable:4514) // 指派不能被产生的运算子 #pragma warning(disable:4512) // 将截断常数值转型 #pragma warning(disable:4310) /Pragma讯息协助程序巨集/ /* 当编译器看到一行如下的程序代码: #pragma chMSG(Fix this later) 它会输出一行如下的讯息: c:/CD/CmnHdr.h(82):Fix this later 您能轻易直接跳至此行并检查周围的程序代码。 */ #define chSTR2(x) #x #define chSTR(x) chSTR2(x) #define chMSG(desc) message(__FILE__"(" chSTR((__LINE__)"):" ##desc) //chINRANGE巨集 // 如果有一个数字在另外二个数字中间,此巨集会回传TRUE #define chINRANGE(low, Num, High)(((low) <= (Num)) && ((Num) <= (High))) chDIMOF巨集 // 此巨集计算阵列中的元件数 #define chDIMOF(Array) (sizeof(Array) / sizeof(Array[0])) /chSIZEOFSTRING巨集 // 此巨集计算一个字串所需的位元组数 #define chSIZEOFSTRING(psz) ((lstrlen(psz) + 1) * sizeof(TCHAR)) ///chROUNDDOWN与chROUNDUP内嵌函数// // 此内嵌函数循环一个减少至最接近的倍数值 template <class TV, class TM> inline TV chROUNDDOWN(TV Value, TM Multiple){ return((Value / Multiple) * Multiple); } // 此内嵌函数循环一个减少至最接近的倍数值 template <class TV, class TM> inline TV chROUNDUP(TV Value, TM Multiple){ return(chROUNDDOWN(Value, Multiple) + (((Value % Multiple) > 0) ? Multiple : 0)); } /chBEGINTHREADEX巨集/// // 此巨集函数呼叫C执行时期的 _beginthreadex函数。 // C执行时期程序库不需依赖任何Windows上的资料 // 如HANDLE的型别。这代表Windows程序设计师在使用 _beginthreadex时需要将这个值转型。 这非常不方便,所以建立了一个巨集执行这个转型动作。 typedef unsigned (__stdcall *PTHREAD_START) (void *); #define chBEGINTHREADEX(psa, cbStack, pfnStartAddr, / pvParam, fdwCreate, pdwThreadId) / ((HANDLE)_beginthreadex( / (void *) (psa), / (unsigned) (cbStack), / (PTHREAD_START) (pfnStartAddr), / (void *) (pvParam), / (unsigned) (fdwCreate), / (unsigned *) (pdwThreadId))) //改善x86平台的DebugBreak/// #ifdef _X86_ #define DebugBreak() _asm { int 3 } #endif ///软件例外巨集// // 建立您自己的软件例外程序代码之有用巨集 #define MAKESOFTWAREEXCEPTION(Severity, Facility, Exception)/ ((DWORD) ( / /* 重要性程序代码 */ (Severity )| / /* MS(0) 或Cust(1) */ (1 << 29)| / /* 保留的(0) */ (0 << 28)| / /* Facility程序代码 */ (Facility << 16)| / /* 例外程序代码 */ (Exception << 0))) ///Quick MessageBox巨集 inline void chMB(PCSTR s){ char szTMP[256]; GetModuleFileNameA(NULL, szTMP, chDIMOF(szTMP)); HWND hwnd = GetActiveWindow(); MessageBoxA(hwnd, s, szTMP, MB_OK | ((hwnd == NULL ) ? MB_SERVICE_NOTIFICATION : 0)); } Assert/Verify巨集/ inline void chMBANDDEBUG(PSTR szMsg){ chMB(szMsg); DebugBreak(); } // 放置一个判断执行失败的讯息方块 inline void chASSERTFAIL(LPCSTR file, int line, PCSTR expr){ char sz[256]; wsprintfA(sz, "File %s, line %d : %s", file, line, expr); chMBANDDEBUG(sz); } // 放置一个在侦错建置时判断执行失败的讯息方块 #ifdef _DEBUG #define chASSERT(x) if (!(x)) chASSERTFAIL(__FILE__, __LINE__, #x) #else #define chASSERT(x) #endif // 在建置侦错时放置一个执行失败的讯息方块 #ifdef _DEBUG #define chFAIL() chASSERTFAIL(__FILE__, __LINE__, "") #else #define chFAIL() #endif // 在侦错建置时判断,不要在建置正式版时移除此程序代码 #ifdef _DEBUG #define chVERIFY(x)chASSERT(x) #else #define chVERIFY(x)(x) #endif ///chHANDLE_DLGMSG巨集/ // 正式的HANDLE_MSG巨集放置在WindowsX.h中,它并不会适当地执行对话方块,因为 DlgProc回传一个BOOL值,取代了LRESULT(像WndProcs一样)。 // 这个chHANDLE_DLGMSG巨集修正了这个问题: #define chHANDLE_DLGMSG(hwnd, message, fn) / case (message): return (SetDlgMsgResult(hwnd, uMsg, / HANDLE_##message((hwnd), (wParam), (lParam), (fn)))) 对话方块图示设定巨集 // 设定对话方块的图示 inline void chSETDLGICONS(HWND hwnd, int idi){ SendMessage(hwnd, WM_SETICON, TRUE, (LPARAM) LoadIcon((HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE), MAKEINTRESOURCE(idi))); SendMessage(hwnd, WM_SETICON, FALSE, (LPARAM) LoadIcon((HINSTANCE)GetWindowLongPtr(hwnd, GWLP_HINSTANCE), MAKEINTRESOURCE(idi))); } /UNICODE检查巨集/ // 由于Windows 98不支援Unicode,如果在Windows 98上执行一个原生的Unicode建置时 ,显示一个错误并中止处理程序。 // 这是经由建立一个全域的C++ 物件完成的。它的建构者在Winmain/main前即已执行 #ifdef UNICODE class CUnicodeSupported { public: CUnicodeSupported(){ if (GetWindowsDirectoryW(NULL, 0) <= 0){ chMB("This application requires an OS that supports Unicode."); ExitProcess(0); } } }; // 「static」让连结器停止了当一个单一项目含有若干来源文件时,物件存在着若干实例 的抱怨情形。 static CUnicodeSupported g_UnicodeSupported; #endif ///OS版本检查巨集/// inline void chWindows9xNotAllowed() { OSVERSIONINFO vi = {sizeof(vi)}; GetVersionEx(&vi); if (vi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS) { chMB("This application requires features not present in Windows 9x."); ExitProcess(0); } } inline void chWindows2000Required() { OSVERSIONINFO vi = { sizeof(vi) }; GetVersionEx(&vi); if ((vi.dwPlatformId != VER_PLATFORM_WIN32_NT) && (vi.dwMajorVersion < 5)){ chMB("This application requires features present in Windows 2000."); ExitProcess(0); } } //SYSTEM_INFO包装程序类别// class CSystemInfo :public SYSTEM_INFO { public: CSystemInfo() {GetSystemInfo(this); } }; / End of File /