第一部分 程式員必讀
第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.cppNotices: Copyright (c) 2000 Jeffrey Richter******************************************************************************/#include "..\CmnHdr.h" /* See Appendix A. */#include <Windowsx.h>#include <tchar.h>#include "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.//#include "resource.h"#define APSTUDIO_READONLY_SYMBOLS///////////////////////////////////////////////////////////////////////////////// Generated from the TEXTINCLUDE 2 resource.//#include "afxres.h"/////////////////////////////////////////////////////////////////////////////#undef APSTUDIO_READONLY_SYMBOLS/////////////////////////////////////////////////////////////////////////////// English (U.S.) resources#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)#ifdef _WIN32LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US#pragma code_page(1252)#endif //_WIN32///////////////////////////////////////////////////////////////////////////////// Dialog//IDD_ERRORSHOW DIALOGEX 0, 0, 182, 42STYLE DS_SETFOREGROUND | DS_3DLOOK | DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENUCAPTION "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_CLIENTEDGEEND///////////////////////////////////////////////////////////////////////////////// DESIGNINFO//#ifdef APSTUDIO_INVOKEDGUIDELINES DESIGNINFO DISCARDABLE BEGIN IDD_ERRORSHOW, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 175 TOPMARGIN, 7 BOTTOMMARGIN, 35 ENDEND#endif // APSTUDIO_INVOKED#ifdef APSTUDIO_INVOKED///////////////////////////////////////////////////////////////////////////////// TEXTINCLUDE//1 TEXTINCLUDE DISCARDABLE BEGIN "resource.h\0"END2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0"END3 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