• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            拂曉·明月·彎刀

            觀望,等待只能讓出現的機會白白溜走

              C++博客 :: 首頁 ::  :: 聯系 :: 聚合  :: 管理 ::
            轉自:http://msdn.microsoft.com/zh-cn/library/ms809695.aspx#mainSection

            發布日期 : 9/9/2004 | 更新日期 : 9/9/2004

            Jon Pincus
            Microsoft Corporation

            摘要:討論錯誤處理范例以及與多個范例相關聯的陷阱,并提供了兩個簡單但至關重要的有關錯誤情況處理的原則。

            本頁內容

            簡介 簡介 
            錯誤處理范例 錯誤處理范例 
            多個 API 約定的問題 多個 API 約定的問題 
            有關錯誤情況的兩個簡單原則 有關錯誤情況的兩個簡單原則 
            小結 小結 

            簡介

            有能力的程序員能夠編寫在未發生異常情況時正常運行的代碼。使程序員出類拔萃的技能之一是能夠編寫在發生錯誤和出現“意外事件”時仍然能繼續運行的代碼。然而,術語“意外事件”會給人一種錯誤的印象。如果您的代碼嵌入在一個廣泛分布的成功產品中,那么您應該預料到代碼可能發生的各種異常(且可怕)的情況。計算機將耗盡內存,文件未如您所愿地存在于應該存在的地方,從未失敗的函數有可能在新版本的操作系統中失敗,等等,不一而足。如果您希望代碼能繼續可靠地運行,那么就需要預見所有這些事件。

            本文討論的特定類別的異常事件是錯誤情況。錯誤情況并沒有一個準確的定義。直觀地講,它就是指通常能夠成功的事情并未成功。內存分配失敗就是一個典型示例。通常,系統有大量內存可以滿足需求,但是偶爾計算機也可能會由于某種原因而特別繁忙,例如,運行大型的電子表格計算,或者在 Internet 世界中有人正在對該計算機發動拒絕服務攻擊。如果您要編寫任意類型的重要組件、服務或應用程序,那么就需要預見到這種情況。

            本文采用的編碼示例基于 C/C++,但一般原則是獨立于編程語言的。即使不使用 C/C++ 代碼,使用多種語言的程序員也應該記住這一點。例如,PC Week 舉辦的黑客競賽 (Hackpcweek.com) 的獲勝者,就是利用某些 Perl CGI 代碼中檢查返回代碼的失敗,來部分地對 Apache 服務器發起攻擊。有關進一步的信息,請參閱 http://www.zdnet.com/eweek/stories/general/0,11011,2350744,00.html 上的文章。

            任何重要的軟件部分都會與其他的層和組件進行交互。編寫處理錯誤的代碼的第一步,是了解當錯誤發生時系統的其余部分正在執行哪些操作。本文的其余部分將討論錯誤處理范例,然后討論與多個范例相關聯的陷阱。本文還包括了兩個簡單但至關重要的有關錯誤情況處理的原則。

            錯誤處理范例

            無論何時發生錯誤情況,都需要考慮三個獨立的問題:檢測錯誤情況、報告錯誤情況以及對錯誤情況作出響應。通常,處理這些問題的職責分散在代碼的不同組件或層中。例如,檢測系統是否內存不足是操作系統的工作。內存分配函數的工作是將這種情況報告給它的調用方。例如,VirtualAlloc 會返回 NULL,Microsoft 基礎類庫 (MFC) 運算符new 會引發CMemoryException *,而 HeapAlloc 可能會返回 NULL 或引發結構化異常。調用方的工作就是對此作出響應,方法是清理自身的工作,捕捉異常,并且還可能將失敗報告給它的調用方或用戶。

            因為不同的層和組件需要相互協作以處理這些錯誤,所以第一步是定義特殊詞匯,即,所有組件共同遵守的約定。遺憾的是,在 C、C++ 或其他任何語言中都沒有單一且定義完善的約定。相反,卻存在許多約定,并且每種約定都有各自的優缺點。

            下面列出了一些最常用的約定:

            • 返回一個 BOOL 值以指示成功或失敗。Windows API 和 MFC 中的很多調用都返回 TRUE 以指示成功,返回 FALSE 以指示失敗。這種方法既好又簡單。但問題在于,該方法不對失敗進行解釋,也不區分不同類型的成功或失敗。(當使用 Windows API 時,您可以使用 GetLastError 來獲得特定代碼。)

            • 返回狀態。遺憾的是,隨著 Windows API 的變化,該約定出現了兩種不同的樣式。COM 函數返回 HRESULT:HRESULT >= 0(例如,S_OK)表示成功,HRESULT < 0(例如,E_FAIL)表示失敗。其他函數(例如,Registry 函數)則返回 WINERROR.H 中定義的錯誤代碼。在該約定中,ERROR_SUCCESS (0) 是唯一的成功值,所有其他值都表示各種形式的失敗。這種不一致有可能造成各種各樣的混亂。更糟糕的是,值 0(它在 Boolean 樣式的返回值約定中表示失敗)在此處表示成功。其后果將在后面進行討論。在任何事件中,狀態返回方法都具有優勢 — 不同的值可以表示不同類型的失敗(或者對于 HRESULT 而言,表示成功)。

            • 返回一個 NULL 指針。C 樣式內存分配函數(例如,HeapAllocVirtualAllocGlobalAlloc  malloc)通常返回一個 NULL 指針。另一個示例是 C 標準庫的 fopen 函數。與 BOOL 返回值相同,還需要其他一些機制來區分不同種類的失敗。

            • 返回一個  不可能的值  該值通常為 0(對于整數,如 GetWindowsDirectory)或 –1(對于指針或長度,如 C 標準庫的 fgets 函數)。NULL 指針約定的泛化是它找到某個值 — 例程采用其他方式將無法返回該值,并且讓該值表示錯誤。因為通常沒有中心模式,所以在將該方法擴展到大型 API 時會在某種程度上出現問題。

            • 引發 C++ 異常。對于純粹的 C++ 程序來說,該約定可讓您使用語言功能來獲益。Bobby Schmidt 最近撰寫的有關異常的“Deep C++”系列文章詳細討論了這些問題。然而,將該方法與舊式的 C 代碼或者與 COM 結合可能會帶來問題。對于 COM 方法而言,引發 C++ 異常是非法的。此外,C++ 異常是開銷相對較大的一種機制。如果操作本身的價值不高,那么過大的開銷通常會抵消所獲得的好處。

            • 引發結構化異常。這里的注意事項恰與 C++ 異常相反。該方法對于 C 代碼而言非常整潔,但與 C++ 的交互性不太好。同樣,該方法不能與 COM 有效地結合。

             如果您要選用較舊的代碼基,那么您有時會看到“原生的異常處理機制”。C++ 編譯器只是在最近才開始比較好地處理異常。在過去,開發人員經常基于 Windows 結構化異常處理 (SEH) 或 C 庫的 setjmp/longjmp 機制來構建他們自己的機制。如果您已經繼承了這些代碼基中的一個,則需要自擔風險,重新編寫它可能是最好的選擇。否則,最好由經驗非常豐富的程序員來處理。

            錯誤處理是任何 API 定義的關鍵部分。無論您是要設計 API 還是使用他人設計的 API,都是如此。對于 API 定義而言,錯誤行為范例與類定義或命名方案同樣重要。例如,MFC API 非常明確地規定了在資源分配失敗時哪些函數引發哪些異常,以及哪些函數返回 BOOL 成功/失敗調用。API 的設計者明確地將某些想法植入這一規定,用戶需要理解其意圖并按照已經構建的規則進行操作。

            如果您要使用現有的 API,則必須處理所有現有約定。如果您要設計 API,則應該選用能夠與您已經使用的 API 相適應的約定。

            如果您要使用多個 API,則通常要使用多個約定。某些約定組合可以很好地工作,因為這些約定很難混淆。但是,某些約定組合卻很容易出錯。本文所討論的許多特定陷阱就是由于存在這些不一致而造成的。

            多個 API 約定的問題

            混用和匹配不同的 API 約定通常是無法避免的,但這非常容易出錯。一些實例是顯而易見的。例如,如果您嘗試在同一個可執行文件中混用 Windows SEH 和 C++ 異常,則很可能會失敗。其他示例更為微妙。其中一個反復出現的特定示例就與 HRESULT 有關,并且是以下示例的某種變體:

            extern BOOL DoIt();
            BOOL ok;
            ok = DoIt(...);
            if (FAILED(ok))     // WRONG!!!
            return;
            

            該示例為何是錯誤的?FAILED 是一個 HRESULT 樣式的宏,因此它會檢查其參數是否小于 0。以下是它的定義(摘自 winerror.h):

            #define FAILED(Status) ((HRESULT)(Status)<0)
            

            因為 FALSE 被定義為 0,所以 FAILED(FALSE) == 0 是違反直覺的,這無須多言。而且,因為該定義嵌入了強制轉換,所以即使您使用警告級別 4,也不會獲得編譯器警告。

            當您處理 BOOL 時,不應該使用宏,但應該進行顯式檢查:

            BOOL ok;
            ok = DoIt(...);
            if (! ok)
            return;
            

            相反,當您處理 HRESULT 時,則應該始終使用 SUCCEEDED 和 FAILED 宏。

            HRESULT hr;
            hr = ISomething->DoIt(...);
            if (! hr)     // WRONG!!!
            return;
            

            這是一個惡性錯誤,因為它很容易被忽略。如果 CoDoIt 返回 S_OK,則測試將成功完成。但是,如果 CoDoIt 返回某個其他成功狀態,會怎樣呢?那樣,hr > 0,所以 !hr == 0;if 測試失敗,代碼將返回實際上并未發生的錯誤。

            下面是另一個示例:

            HRESULT hr;
            hr = ISomething->DoIt(...);
            if (hr == S_OK)     // STILL WRONG!!!
            return;
            

            人們有時會插話說 ISomething::DoIt 在成功時總是返回 S_OK,因此最后兩個代碼片段肯定都沒有問題。但是,這不是一個安全的假設。COM 接口的說明非常清楚。函數在成功時可以返回任何成功值,因此 ISomething::DoIt 的眾多實現者中的任何一個都可能選擇返回某個值,例如 S_FALSE。在這種情況下,您的代碼將中止運行。

            正確的解決方案是使用宏,這也就是宏存在的原因。

            HRESULT hr;
            hr = ISomething->DoIt(...);
            if (FAILED(hr))
            return;
            

            因為已經引出了 HRESULT 的主題,所以現在是提醒您 S_FALSE 特性的大好時機:

            • 它是一個成功代碼,而不是一個失敗代碼,因此 SUCCEEDED(S_FALSE) == 1。

            • 它被定義為 1,而不是 0,因此 S_FALSE == TRUE。

            有關錯誤情況的兩個簡單原則

            有許多簡單的方法可以使代碼更可靠地處理錯誤情況。相反,人們不愿意做的許多簡單事情會使代碼在發生錯誤情況時變得脆弱和不可靠。

            總是檢查返回狀態

            沒有比這更簡單的事情了。幾乎所有函數都提供某種表明它們是成功還是失敗的指示,但如果您不檢查它們,則這一點沒有任何用處。這能有多困難呢?可以將其視為一個衛生問題。您知道在吃東西之前應該洗手,但您可能并不總是這樣做。這與檢查返回值的道理相同。

            下面是一個涉及到 GetWindowsDirectory 函數的簡單而實用的示例。MSDN 文檔清楚地說明了 GetWindowsDirectory 的錯誤行為:

            Return Values
            

            如果該函數失敗,則返回值為 0。要獲得擴展的錯誤信息,請調用 GetLastError 函數。

            實際上,文檔中寫得非常清楚。

            下面是一個判斷 Windows 目錄駐留在哪個驅動器中的代碼片段。

            TCHAR cDriveLetter;
            TCHAR szWindowsDir[MAX_PATH];
            GetWindowsDirectory(szWindowsDir, MAX_PATH);
            cDriveLetter = szWindowsDir[0];   // WRONG!!!
            ...
            

            如果 GetWindowsDirectory 失敗,會發生什么情況呢?(如果您不相信 GetWindowsDirectory 會失敗,這只是您暫時的觀點。)好,該代碼不檢查返回值,因此分配給 cDriveLetter 的值未初始化。未初始化的內存可以具有任意值。實際上,該代碼將隨機選擇驅動器。這樣做幾乎不可能是正確的。

            正確的做法是檢查錯誤狀態。

            TCHAR cDriveLetter;
            TCHAR szWindowsDir[MAX_PATH];
            if (GetWindowsDirectory(szWindowsDir, MAX_PATH))
            {
            cDriveLetter = szWindowsDir[0];
            ...
            }
            

            這種情況還能發生嗎?不檢查返回值的最常見借口是“我知道那個函數絕對不會失敗”。GetWindowsDirectory 就是一個很好的示例。一直到 Windows® 98 和 Windows NT® 4.0,它實際上確實沒有失敗過,因此許多人養成了一個不好的習慣,即,假設它永遠不會失敗。

            現在 Windows 終端服務器出現了,要判斷單個用戶的 Windows 目錄變得更為復雜。GetWindowsDirectory 必須完成更多的工作,可能包括分配內存。而且,因為開發這一函數的開發人員非常負責任,所以他完成了正確的工作并檢查內存分配是否成功,如果不成功,則返回描述完整的錯誤狀態。

            這就導致了另外一些問題:如果 GetWindowsDirectory 在失敗時已經將它的輸出初始化為空字符串,是否會有所幫助?答案是否定的。結果不會是未初始化的,但它們仍將是粗心大意的應用程序所未曾料到的東西。假設您具有一個由 cDriveLetter – 'A' ; 索引的數組,那么現在該索引將突然變為負值。

            即使終端服務器對于您不是問題,但同樣的情況可能會發生在 Windows API 的任何未來實現中。您希望禁止正在開發的應用程序在將來版本的操作系統或替代實現(如 Embedded NT)中運行嗎?一種良好的習慣是記住以下事實:代碼經常在其預期的到期日之后繼續生存。

            有時,檢查返回值是不夠的。請考慮 Windows API ReadFile。您經常會看到如下代碼:

            LONG buffer[CHUNK_SIZE];
            ReadFile(hFile, (LPVOID)buffer,
            CHUNK_SIZE*sizeof(LONG), &cbRead, NULL);
            if (buffer[0] == 0)   // DOUBLY WRONG!!!
            ...
            

            如果讀取操作失敗,說明緩沖區的內容是未初始化的。多數情況下它可能為零,但這一點并不確定。

            讀取文件失敗的原因有許多。例如,該文件可能是遠程文件,而網絡可能發生故障。即使它是本地文件,磁盤也可能恰好不合時宜地損壞。如果是這種情況,則文件的格式可能完全不同于預期格式。他人可能無意中或別有用心地替換了您認為應該存在于某個位置的文件,或者該文件可能只有一個字節。更為奇怪的事情已經發生了。

            要處理這一情況,您不但需要檢查讀取操作是否成功,還必須檢查以確保您已經讀取了正確數量的字節。

            LONG buffer[CHUNK_SIZE];
            BOOL ok;
            ok = ReadFile(hFile, (LPVOID)buffer,
            CHUNK_SIZE*sizeof(LONG), &cbRead, NULL);
            if (ok && cbRead > sizeof(LONG)) {
            if (buffer[0] == 0)
            ...
            }
            else
            // handle the read failure; for example
            ...
            

            無庸諱言,上述代碼有點兒復雜。但是,編寫可靠的代碼要比編寫并不總是能夠正常工作的代碼更為復雜。對上述代碼產生性能方面的疑問是很正常的。雖然添加了幾個測試,但在全局上下文(函數調用、磁盤操作,至少復制 CHUNK_SIZE * sizeof(LONG) 個字節)中,其影響是極小的。

            通常,每當需要進行返回值檢查時,總是涉及到一個函數調用,因此性能開銷不太重要。在某些情況下,編譯器可能會內聯該函數,但是如果發生這種行為,并且由于返回常數而實際上不需要檢查返回值時,編譯器會將測試優化掉。

            誠然,還是有一些特殊情況:您通過刪除返回值檢查而節省的少數 CPU 循環至關重要;編譯器無法為您提供幫助;您控制了要調用的函數的行為。在上述情況下,省略一些返回值檢查是有意義的。如果您認為自己處于類似的情形,則應該與其他開發人員進行討論,重新審視真正的性能折衷,然后,如果您仍然確信這樣做是正確的,則在代碼中每個省略返回值檢查的地方加上明確的注釋,說明您的決定并證明它的正確性。

            總是檢查內存分配

            無論是使用 HeapAllocVirtualAllocIMalloc::AllocSysAllocStringGlobalAllocmalloc 還是任何 C++ 運算符 new,您都不能想當然地認為內存分配成功。同樣的道理也適用于其他各種資源,包括 GDI 對象、文件、注冊表項等等。

            下面是一個很好的獨立于平臺的錯誤代碼示例,這些代碼是在 C 標準庫的基礎上編寫的:

            char *str;
            str = (char *)malloc(MAX_PATH);
            str[0] = 0;         // WRONG!!!
            ...
            

            在此例中,如果內存耗盡,則 malloc 將返回 NULL,而 str 的反引用將是 NULL 指針的反引用。這會造成訪問沖突,從而導致程序崩潰。除非您是在內核模式下運行(例如,設備驅動程序),否則訪問沖突將導致藍屏或可利用的安全漏洞。

            解決方案非常簡單。檢查返回值是否為 NULL,并執行正確的操作。

            char *str;
            str = (char *)malloc(MAX_PATH);
            if (str != NULL)
            {
            str[0] = 0;
            ...
            }
            

            與返回值檢查一樣,許多人相信實際上不會發生內存分配問題。誠然,該問題并不總是發生,但這并不意味著它永遠不會發生。如果您讓成千上萬(或數以百萬)的用戶運行您的軟件,即使該問題每月僅對每個用戶發生一次,后果也是嚴重的。

            許多人相信,在內存耗盡時做什么都是無所謂的。程序應該退出。但是,這在許多方面都不適用。首先,假設程序在內存耗盡時退出,那么將不會保存數據文件。其次,人們通常期望服務和應用程序能夠長期運行,因此它們能夠在內存暫時不足時繼續正常運行是至關重要的。第三,對于在嵌入式環境中運行的軟件而言,退出不是可行的選擇。處理內存分配可能非常麻煩,但這件事情必須做。

            有時,意外的 NULL 指針可能不會導致程序崩潰,但這仍然不是件好事情。

            HBITMAP hBitmap;
            HBITMAP hOldBitmap;
            hBitmap = CreateBitmap(. . .);
            hOldBitmap = SelectObject(hDC, hBitmap);   // WRONG!!!
            ...
            

            SelectObject 的文檔在對 NULL 位圖執行哪些操作方面含糊不清。這可能不會導致崩潰,但它顯然是不可靠的。代碼很可能出于某種原因而創建位圖,并希望用它來進行一些繪圖工作。但是,因為它未能創建位圖,所以繪圖操作將不會發生。即使代碼沒有崩潰,這里也明顯存在一個錯誤。同樣,您需要進行檢查。

            HBITMAP hBitmap;
            HBITMAP hOldBitmap;
            hBitmap = CreateBitmap(. . .);
            if (hBitmap != NULL)
            {
            hOldBitmap = SelectObject(hDC, hBitmap);
            ...
            }
            else
            ...
            

            當您使用 C++ 運算符 new 時,事情開始變得更加有趣。例如,如果您要使用 MFC,則全局運算符 new 將在內存耗盡時引發一個異常。這意味著您不能執行以下操作:

            int *ptr1;
            int *ptr2;
            ptr1 = new int[10];
            ptr2 = new int[10];   // WRONG!!!!
            

            如果第二次內存分配引發異常,則第一次分配的內存將泄漏。如果您的代碼嵌入到將要長期運行的服務或應用程序中,則這些泄漏會累積起來。

            只是捕捉異常是不夠的,您的異常處理代碼還必須是正確的。不要掉到下面這個誘人的陷阱中:

            int *ptr1;
            int *ptr2;
            try {
            ptr1 = new int[10];
            ptr2 = new int[10];
            }
            catch (CMemoryException *ex) {
            delete [] ptr1;   // WRONG!!!
            delete [] ptr2;    // WRONG!!!
            }
            

            如果第一次內存分配引發了異常,您將捕捉該異常,但要刪除一個未初始化的指針。如果您足夠幸運,這將導致即時訪問沖突和崩潰。更有可能的是,它將導致堆損壞,從而造成數據損壞和/或在將來難以調試的崩潰。盡力初始化下列變量是值得的:

            int *ptr1 = 0;
            int *ptr2 = 0;
            try {
            ptr1 = new int[10];
            ptr2 = new int[10];
            }
            catch (CMemoryException *ex) {
            delete [] ptr1;
            delete [] ptr2;
            }
            

            應該指出的是,C++ 運算符 new 有許多微妙之處。您可以用多種不同的方式來修改全局運算符 new 的行為。不同的類可以具有它們自己的運算符 new,并且如果您不使用 MFC,則可能會看到不同的默認行為。例如,在內存分配失敗時返回 NULL,而不是引發異常。有關該主題的詳細信息,請參閱 Bobby Schmidt 的“Deep C++”系列文章中有關處理異常的第 7 部分

            小結

            如果您要編寫可靠的代碼,則至關重要的一點是從一開始就考慮如何處理異常事件。您不能事后再考慮對異常事件的處理。在考慮此類事件時,錯誤處理是一個關鍵的方面。

            錯誤處理很難正確執行。盡管本文只是粗淺地討論了這一問題,但其中介紹的原則奠定了一個強大的基礎。請記住以下要點:

            • 在設計應用程序(或 API)時,應預先考慮您喜歡的錯誤處理范例。

            • 在使用 API 時,應了解它的錯誤處理范例。

            • 如果您處于存在多個錯誤處理范例的情況下,請警惕可能造成混亂的根源。

            • 總是檢查返回狀態。

            • 總是檢查內存分配。

            如果您執行了上述所有操作,就能夠編寫出可靠的應用程序。

            posted on 2011-04-19 23:40 一路風塵 閱讀(309) 評論(0)  編輯 收藏 引用 所屬分類: 轉載
            久久久久久免费一区二区三区| 国内精品伊人久久久久影院对白 | 久久夜色精品国产噜噜噜亚洲AV| 久久久精品人妻一区二区三区蜜桃| 精品久久亚洲中文无码| 久久婷婷激情综合色综合俺也去| AV狠狠色丁香婷婷综合久久| AV无码久久久久不卡蜜桃| 久久婷婷色综合一区二区| 精品国产青草久久久久福利| 女人香蕉久久**毛片精品| 中文字幕无码久久久| 99久久精品午夜一区二区| 久久久青草青青国产亚洲免观| 伊人久久综合无码成人网| 丁香五月综合久久激情| 狼狼综合久久久久综合网| 久久精品国产亚洲av瑜伽| 97久久精品国产精品青草| 久久久黄色大片| 国产福利电影一区二区三区久久久久成人精品综合 | 久久WWW免费人成一看片| 999久久久国产精品| 久久精品www人人爽人人| 久久综合伊人77777麻豆| 久久天堂电影网| 精品久久久久久成人AV| 成人午夜精品无码区久久| 无码任你躁久久久久久久| 亚洲综合精品香蕉久久网97| 久久婷婷激情综合色综合俺也去| 99精品国产免费久久久久久下载| 激情综合色综合久久综合| 亚洲国产精品婷婷久久| 97久久超碰国产精品2021| 伊人久久综合无码成人网| 国产精品久久久久免费a∨| 欧美色综合久久久久久| 久久免费观看视频| 日韩亚洲国产综合久久久| 成人a毛片久久免费播放|