SEH 與 C++ 異常模型的混合使用

轉載自-http://blog.programfan.com/article.asp?id=9865

在上一篇文章中我們看到了,在 C++ 程序中可以能夠很好地使用 SEH 的 try-except 和 try-finally 機制(雖然 MSDN 中不建議這樣做),這一篇文章中我們繼續討論,在 C++ 程序中同時使用 try-except 異常機制( SEH )和 try-catch 異常機制( C++ 異常模型 )的情況。

朋友們,準備好了心情嗎?這可是有點複雜呦!

如何混合使用呢?

同樣,還是看例子先。仍然是在原來例程的代碼基礎上做修改,修改後的代碼如下:

// 注意,這是 C++ 程序,文件名爲: SEH-test.cpp

#include "stdio.h"

class A

{

public:

void f1() {}

// 拋出 C++ 異常

void f2() { throw 888;}

};

// 這個函數中使用了 try-catch 處理異常,也即 C++ 異常處理

void test1()

{

A a1;

A a2,a3;

try

{

a2.f1();

a3.f2();

}

catch(int errorcode)

{

printf("catch exception,error code:%d/n", errorcode);

}

}

// 這個函數沒什麼改變,仍然採用 try-except 異常機制,也即 SEH 機制

void test()

{

int* p = 0x00000000; // pointer to NULL

__try

{

// 這裏調用 test1 函數

test1();

puts("in try");

__try

{

puts("in try");

// causes an access violation exception;

// 導致一個存儲異常

*p = 13;

puts(" 這裏不會被執行到 ");

}

__finally

{

puts("in finally");

}

puts(" 這裏也不會被執行到 ");

}

__except(puts("in filter 1"), 0)

{

puts("in except 1");

}

}

void main()

{

puts("hello");

__try

{

test();

}

__except(puts("in filter 2"), 1)

{

puts("in except 2");

}

puts("world");

}

上面程序不僅能夠被編譯通過,而且運行結果也是正確的(和預期的一樣,同樣符合 C++ 異常處理模型的規則,和 SEH 異常模型的處理規則)。其結果如下:

hello

catch exception,error code:888

in try

in try

in filter 1

in filter 2

in finally

in except 2

world

Press any key to continue

繼續深入剛纔的例子

上面的例程中,雖然在同一個程序中既有 try-catch 機制,又有 try-except 機制,當然這也完全算得上 SEH 與 C++ 異常模型的混合使用 。但是,請注意,這兩種機制其實是完全被分割開的,它們完全被分割在了兩個函數的內部。也即這兩種機制其實並沒有完全交互起來,換句話說,它們還算不上兩種異常處理機制真正的混合使用。這裏繼續給出一個更絕的例子,還是先來看看代碼吧,如下:

// 注意,這是 C++ 程序,文件名爲: SEH-test.cpp

#include "stdio.h"

class MyException

{

public:

MyException() {printf(" 構造一個 MyException 對象 /n");}

MyException(const MyException& e) {printf(" 複製一個 MyException 對象 /n");}

operator=(const MyException& e) {printf(" 複製一個 MyException 對象 /n");}

~MyException() {printf(" 析構一個 MyException 對象 /n");}

};

class A

{

public:

A() {printf(" 構造一個 A 對象 /n");}

~A() {printf(" 析構一個 A 對象 /n");}

void f1() {}

// 注意,這裏拋出了一個 MyException 類型的異常對象

void f2() {MyException e; throw e;}

};

// 這個函數中使用了 try-catch 處理異常,也即 C++ 異常處理

void test1()

{

A a1;

A a2,a3;

try

{

a2.f1();

a3.f2();

}

// 這裏雖然有 catch 塊,但是它捕獲不到上面拋出的 C++ 異常對象

catch(int errorcode)

{

printf("catch exception,error code:%d/n", errorcode);

}

}

// 這個函數沒什麼改變,仍然採用 try-except 異常機制,也即 SEH 機制

void test()

{

int* p = 0x00000000; // pointer to NULL

__try

{

// 這裏調用 test1 函數

// 注意, test1 函數中會拋出一個 C++ 異常對象

test1();

puts("in try");

__try

{

puts("in try");

*p = 13;

puts(" 這裏不會被執行到 ");

}

__finally

{

puts("in finally");

}

puts(" 這裏也不會被執行到 ");

}

__except(puts("in filter 1"), 0)

{

puts("in except 1");

}

}

void main()

{

puts("hello");

__try

{

test();

}

// 這裏能捕獲到 C++ 異常對象嗎?拭目以待吧!

__except(puts("in filter 2"), 1)

{

puts("in except 2");

}

puts("world");

}

仔細閱讀上面的程序,不難看出, SEH 與 C++ 異常模型兩種機制確實真正地交互起來了,上層的 main() 函數和 test() 函數採用 try-except 語句處理異常,而下層的 test1() 函數採用標準的 try-catch 語句處理異常,並且,下層的 test1() 函數所拋出的 C++ 異常會被上層的 try-except 所捕獲到嗎?還是看運行結果吧! 如下:

hello

構造一個 A 對象

構造一個 A 對象

構造一個 A 對象

構造一個 MyException 對象

複製一個 MyException 對象

in filter 1

in filter 2

析構一個 MyException 對象

析構一個 A 對象

析構一個 A 對象

析構一個 A 對象

in except 2

world

Press any key to continue

結果是否和朋友們的預期一致呢?它的的確確是上層的 try-except 塊,能夠捕獲到下層的 test1() 函數所拋出的 C++ 異常,而且流程還是正確的,即符合了 SEH 異常模型的規則,又同時遵循了 C++ 異常模型的規則。同時,最難能可貴的是,在 test1() 函數中的三個局部變量,都被正確的析構了(這非常神奇吧!具體的機制這裏暫且不詳細論述了,在後面闡述“異常機制的實現”的文章中再做論述)。

細心的程序員朋友們,也許從上面程序的運行結果中發現了一些“問題”,什麼呢?那就是“ MyException 對象 ”構造了兩次,但它只被析構了一次。呵呵!這也許就是 MSDN 中不建議混合使用這兩種異常處理機制的背後原因之一吧!雖然說,這種問題不至於對整個程序造成很大的破壞性影響,但主人公阿愚卻堅持認爲,如果我們編程時濫用 try-except 和 try-catch 在一起混用,不僅使我們程序的整體結構和語義受到影響,而且也會造成一定的內存資源泄漏,甚至其它的不穩定因素。

總之,在 C++ 程序中運用 try-except 機制,只有在頂層的函數作用域中(例如,系統運行庫中,或 plugin 的鉤子中)纔有必要這樣做。如在 VC 編寫的程序中,每當我們程序運行中出現意外異常導致的崩潰事件時,系統總能夠彈出一個“應用程序錯誤”框,如下:

NT 操作系統是如何實現的呢?很簡單,它就是在在 VC 運行庫中的 頂層的函數內採用了 try-except 機制,不信,看看如下截圖代碼吧!

C++ 異常處理模型能捕獲 SEH 異常嗎?

呵呵!阿愚笑了,這還用問嗎?當然了, VC 提供的 C++ 異常處理模型的強大之處就是,它不僅能捕獲 C++ 類型的異常,而且它還能捕獲屬於系統級別的 SEH 異常。它就是利用了 catch(…) 語法,在前面專門闡述 catch(…) 語法時,我們也着重論述了這一點。不過,這裏還是給出一個實際的例子吧,代碼如下:

class MyException

{

public:

MyException() {printf(" 構造一個 MyException 對象 /n");}

MyException(const MyException& e) {printf(" 複製一個 MyException 對象 /n");}

operator=(const MyException& e) {printf(" 複製一個 MyException 對象 /n");}

~MyException() {printf(" 析構一個 MyException 對象 /n");}

};

class A

{

public:

A() {printf(" 構造一個 A 對象 /n");}

~A() {printf(" 析構一個 A 對象 /n");}

void f1() {}

// 拋出 C++ 異常

void f2() {MyException e; throw e;}

};

void test()

{

int* p = 0x00000000; // pointer to NULL

__try

{

puts("in try");

__try

{

puts("in try");

// causes an access violation exception;

// 導致一個存儲異常

*p = 13;

// 呵呵,注意這條語句

puts(" 這裏不會被執行到 ");

}

__finally

{

puts("in finally");

}

// 呵呵,注意這條語句

puts(" 這裏也不會被執行到 ");

}

__except(puts("in filter 1"), 0)

{

puts("in except 1");

}

}

void test1()

{

A a1;

A a2,a3;

try

{

// 這裏會產生一個 SEH 類型的系統異常

test();

a2.f1();

a3.f2();

}

// 捕獲得到嗎?

catch(...)

{

printf("catch unknown exception/n");

}

}

void main()

{

puts("hello");

__try

{

test1();

}

__except(puts("in filter 2"), 0)

{

puts("in except 2");

}

puts("world");

}

上面的程序很簡單的,無須進一步討論了。當然,其實我們還可以更進一步深入進去,因爲 C++ 異常處理模型不僅能夠正常捕獲到 SEH 類型的系統異常,而且它還能夠把 SEH 類型的系統異常轉化爲 C++ 類型的異常。我想,這應該放在單獨的一篇文章中來闡述了,其實這在許多關於 Window 系統編程的書籍中也有詳細討論。

SEH 與 C++ 異常模型在混合使用時的“禁區”

剛纔我們看到,利用 try-except 來捕獲 C++ 異常有點小問題,但這畢竟算不上什麼禁區。那麼,何爲 SEH 與 C++ 異常模型在混合使用時的“禁區”呢?看個例子吧,代碼如下:

// 注意,這是 C++ 程序,文件名爲: SEH-test.cpp

#include "stdio.h"

void main()

{

int* p = 0x00000000; // pointer to NULL

// 這裏是 SEH 的異常處理語法

__try

{

puts("in try");

// 這裏是 C++ 的異常處理語法

try

{

puts("in try");

// causes an access violation exception;

// 導致一個存儲異常

*p = 13;

// 呵呵,注意這條語句

puts(" 這裏不會被執行到 ");

}

catch(...)

{

puts("catch anything");

}

// 呵呵,注意這條語句

puts(" 這裏也不會被執行到 ");

}

__except(puts("in filter 1"), 1)

{

puts("in except 1");

}

}

朋友們!不要急於編譯並測試上面的小程序,先猜猜它會有什麼結果呢?想到了嗎?不妨實踐一下,呵呵!實際結果是否令你喫驚呢?對了,沒錯, VC 就是會報出一個編譯錯誤(“ error C2713: Only one form of exception handling permitted per function ”)。那麼原因何在呢?主人公阿愚在此一定“知無不言,言無不盡”,這是因爲: VC 實現的異常處理機制,不管是 try-except 模型,還是 try-catch 模型,它們都是以函數作爲一個最基本“分析和控制”的目標,也即,如果一個函數內使用了異常處理機制, VC 編譯器在編譯該函數時,它會給此函數插入一些“代碼和信息”(代碼指的是當該函數中出現異常時的回調函數,而信息主要是指與異常出現相關的一些必要的鏈表),因此每份函數只能有一份這樣的東東(“代碼和信息”),故一個函數只能採用一種形式的異常處理規則。 朋友們!恍然大悟了吧!其實這倒不算最令人不可思議的。還有一種更爲特殊的情況,看下面的例子,代碼如下:

class A

{

public:

A() {printf(" 構造一個 A 對象 /n");}

~A() {printf(" 析構一個 A 對象 /n");}

void f1() {}

void f2() {}

};

void main()

{

__try

{

A a1, a2;

puts("in try");

}

__except(puts("in filter 1"), 1)

{

puts("in except 1");

}

}

其實上面的程序表面上看,好像是沒有什麼特別的嗎?朋友們!仔細看看,真的沒什麼特別的嗎?不妨編譯一下該程序,又奇怪了吧!是的,它同樣也編譯報錯了,這是我機器上編譯時產生的信息,如下:

--------------------Configuration: exception - Win32 Debug--------------------

Compiling...

seh-test.cpp

f:/exception/seh-test.cpp(214) : warning C4509: nonstandard extension used: 'main' uses SEH and 'a2' has destructor

f:/exception/seh-test.cpp(211) : see declaration of 'a2'

f:/exception/seh-test.cpp(214) : warning C4509: nonstandard extension used: 'main' uses SEH and 'a1' has destructor

f:/exception/seh-test.cpp(211) : see declaration of 'a1'

f:/exception/seh-test.cpp(219) : error C2712: Cannot use __try in functions that require object unwinding

Error executing cl.exe.

Creating browse info file...

exception.exe - 1 error(s), 2 warning(s)

那麼,上面的錯誤信息代表什麼意思,我想是不是有不少朋友都遇到過這種莫名奇妙的編譯問題。其實,這確實很令人費解,明明程序很簡單的嗎?而且程序中只用到了 SEH 異常模型的 try-except 語法,甚至 SEH 與 C++ 異常模型兩者混合使用的情況都不存在。那麼編譯出錯的原因究竟何在呢?實話告訴你吧!其實主人公阿愚在此問題上也是費透了腦筋,經過了一番深入而細緻的鑽研之後,才知道真正原因的。

那原因就是: 同樣還是由於在一個函數不能採用兩種形式的異常處理機制而導致的編譯錯誤。 啊!這豈不是更迷惑了。其實不然,說穿了,朋友們就會明白了,這是因爲: 在 C++ 異常處理模型中,爲了能夠在異常發生後,保證正確地釋放相關的局部變量(也即調用析構函數),它必須要跟蹤每一個“對象”的創建過程,這種由於異常產生而導致的對象析構的過程,被稱爲“ unwind ”(記得前面的內容中,也多次講述到了這個名詞),因此,如果一個函數中有局部對象的存在,那麼它就一定會存在 C++ 的異常處理機制(也即會給此函數插入一些用於 C++ 異常處理“代碼和信息”),這樣,如果該函數中在再使用 try-except 機制,豈不是就衝突了嗎?所以編譯器也就報錯了,因爲它處理不了了! 哈哈!朋友們,主人公阿愚把問題說清楚了嗎?

總結

•  SEH 與 C++ 異常模型,可以在一起被混合使用。但最好聽從 MSDN 的建議:在 C 程序中使用 try-except 和 try-finally ;而 C++ 程序則應該使用 try-catch 。

•  混合使用時, C++ 異常模型可以捕獲 SEH 異常;而 SEH 異常模型也可以捕獲 C++ 類型的異常。而後者通常有點小問題,它一般主要運用在提高和保證產品的可靠性上(也即在頂層函數中使用 try-except 語句來 catch 任何異常)

•  VC 實現的異常處理機制中,不管是 try-except 模型,還是 try-catch 模型,它們都是以函數作爲一個最基本“分析和控制”的目標,也即一個函數中只能採用一種形式的異常處理規則。否則,編譯這一關就會被“卡殼”。

下一篇文章中,主人公阿愚打算接着詳細討論一些關於 C++ 異常處理模型的高級使用技巧,也即 “ 如何把 SEH 類型的系統異常轉化爲 C++ 類型的異常? ”, 程序員朋友們,繼續吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章