使用 VC++.NET 进行托管异常处理






4.91/5 (23投票s)
2003 年 8 月 9 日
7分钟阅读

128329
MC++ 程序中托管异常处理的介绍。
引言
C++ 的托管扩展扩展了传统的 C++ 异常处理功能,并增加了对托管异常处理的支持。本文不讨论传统异常处理或结构化异常处理,仅作为在应用程序中使用 C++ 托管扩展处理托管异常的指南。托管异常是由托管类型(通常是 BCL 中的类)抛出的异常,建议您使用 System::Exception
类作为您的异常对象,可以直接使用,也可以通过将该类作为基类来创建自定义异常类。虽然没有什么可以阻止您抛出非派生自 System::Exception
的托管对象;但需要注意的一点是,您只能抛出 __gc
对象,这意味着您不能直接抛出 __value
对象(但可以对其进行装箱然后抛出装箱后的对象)。
注意 - 在所有示例代码片段中,您会看到一个 Show
函数,它实际上并不是一个函数,而是我添加的一个预处理器定义,如下所示:
#define Show Console::WriteLine
抛出异常
使用 throw
语句抛出托管扩展。
void ThrowFunction()
{
Show("in ThrowFunction");
throw new Exception("asdf");
}
在上面的示例中,我抛出了一个 System::Exception
类型的异常。 System::Exception
是所有托管异常类的推荐基类,.NET BCL 类专门使用该类或其几个派生类来满足异常处理需求。调用上述函数将产生非常类似于以下内容的输出:
in ThrowFunction
Unhandled Exception: System.Exception: asdf
at ThrowFunction() in c:\...\exceptionsdemo.cpp:line 23
at main() in c:\...\exceptionsdemo.cpp:line 138
由于我们没有处理异常,CLR 为我们处理了它,它显示了我们在创建异常时使用的构造函数重载传递给它的异常文本消息。本质上,接受 System::String
的 Exception
构造函数只是将传入的字符串保存到一个名为 _message
的成员变量中,并通过一个名为 Message
的只读属性暴露出来。此外,CLR 还通过使用 Exception
类的 StackTrace
属性打印堆栈跟踪。所以我们可以设想,CLR 为在它之下运行的所有 IL 可执行文件设置了一个捕获所有异常的处理程序,对于捕获到的异常,它只是打印 Exception
对象(已抛出)的 Message
和 StackTrace
属性中的文本。当然,这是对 CLR 活动的一种过度简化的描述,实际的 CLR 过程可能比这个简单的可视化复杂得多。
异常处理
我们通过使用 try
-catch
块来处理异常。
void TryFunction()
{
try
{
Show("in TryFunction");
ThrowFunction();
Show("this won't come");
}
catch( Exception* e)
{
Show("in TryFunction exception block {0}",e->Message);
}
}
上述程序的输出将类似于:
in TryFunction
in ThrowFunction
in TryFunction exception block asdf
发生的情况是,try
块内的代码将执行,直到发生异常或到达 try
块的末尾,如果发生异常,控制将跳转到 catch
块。 try
块中剩余的任何代码都不会执行,这从上述代码片段的输出中可以明显看出。 ThrowFunction
函数与早期代码片段中的相同,我们通过 System::Exception
类的 Message
属性显示异常消息。
在上述场景中可能出现的一个问题是,您需要执行清理操作。假设您打开了一个磁盘文件,在向磁盘文件写入内容时引发了异常,这意味着控制已跳转到 catch
块。现在,关闭磁盘文件的代码不会执行。一个相当笨拙的解决方案是在 try
-catch
块之外关闭文件:
if( file_is_open )
{
//close file
}
乍一看这似乎没问题,但假设您有嵌套的 try
-catch
块(在编写一些规范代码时这很正常),现在您需要小心地确定在哪里释放资源,并且您可能还需要增加临时资源句柄变量的作用域,以便在 try
-catch
块之外访问它们。解决此问题的方法是使用 __finally
关键字来创建 __finally
块,无论是否引发异常,这些块都会被执行。
void TryFunction()
{
try
{
Show("in TryFunction");
ThrowFunction();
}
catch( Exception* e)
{
Show("in TryFunction exception block {0}",e->Message);
}
__finally
{
Show("in TryFunction Finally block");
}
}
现在尝试运行上面的代码一次,然后注释掉 try
块中的 ThrowFunction
函数调用后再次运行它。您会看到在两种情况下,__finally
块内的代码都会执行。
重新抛出异常
当您在一个 try
-catch
块中捕获异常时,会有一个小问题——位于 try
-catch
嵌套层级中更上面的 try
-catch
块将看不到此异常。幸运的是,重新抛出异常非常容易。
void TryFunction()
{
try
{
Show("in TryFunction");
ThrowFunction();
}
catch( Exception* e)
{
Show("in TryFunction exception block {0}",e->Message);
throw;//rethrow the exception
}
__finally
{
Show("in TryFunction Finally block");
}
}
void TryFunction2()
{
try
{
Show("in TryFunction2");
TryFunction();
}
catch(Exception* e)
{
Show("in TryFunction2 exception block {0}",e->Message);
}
__finally
{
Show("in TryFunction2 Finally block");
}
}
我们在内部 catch
块中使用不带参数的 throw
语句。这将把相同的异常对象传播到外部 try
-catch
块,这从调用 TryFunction2
方法的输出中可以看出。
in TryFunction2
in TryFunction
in ThrowFunction
in TryFunction exception block asdf
in TryFunction Finally block
in TryFunction2 exception block asdf
in TryFunction2 Finally block
如果您注释掉内部 catch
块中的 throw
语句,您将看到外部 catch
块永远不会被执行,因为异常在内部 try
-catch
块中得到了处理,但两个 __finally
块都按预期执行。
使用自定义异常类
您可以创建自己的异常类,并添加额外的属性和方法以满足您的需求,强烈建议您派生自 System::Exception
。您还必须记住为自定义异常类实现的每个构造函数重载显式指定相应的基类构造函数。
public __gc class MyException : public Exception
{
public:
MyException() : Exception()
{
//stuff
}
MyException(String* str) : Exception(str)
{
//stuff
}
private:
//custom members and functions
};
void ThrowFunction()
{
Show("in ThrowFunction");
throw new MyException("asdf");
}
void TryFunction()
{
try
//...snipped...
catch( MyException* e)
{
Show("in TryFunction exception block {0}",e->Message);
throw;
}
//...snipped...
}
void TryFunction2()
{
try
//...snipped...
catch(MyException* e)
{
Show("in TryFunction2 exception block {0}",e->Message);
}
//...snipped...
}
我只是从 System::Exception
派生了一个名为 MyException
的类,并重写了基类的两个重载构造函数。现在,在我的 catch 块中,我将 MyException
指定为要捕获的异常对象的类型。
多个 catch 块
在一个 try
-catch
-__finally
代码块中可以有多个 catch
块,我们可以利用这一点来专门处理抛出的多种异常类型。让我们编写一个函数,该函数根据传递给它的参数抛出不同类型的异常:
void ThrowFunction2(int i)
{
Show("in ThrowFunction2");
if( i == 0 )
throw new MyException("asdf");
else
if( i == 1 )
throw new Exception("asdf");
else
throw new Object();
}
现在我们可以像下面这样单独处理这些不同的异常场景:
void TryFunction3(int i)
{
try
{
Show("in TryFunction3");
ThrowFunction2(i);
}
catch( MyException* e)
{
Show("in TryFunction3 my exception block {0}",e->Message);
}
catch( Exception* e)
{
Show("in TryFunction3 exception block {0}",e->Message);
}
catch( Object* o)
{
Show("in TryFunction3 exception block {0}",o->ToString());
}
__finally
{
Show("in TryFunction3 Finally block");
}
}
请记住将 catch
块按照继承层次结构的逆序排列,否则继承链中的上层类将被继承链中最低的类或其下任何类所吞噬。例如,在上面的代码片段中,如果我们把 Exception
catch
块放在 MyException
catch
块的上面,它将捕获 Exception
类型和 MyException
类型的异常,这将是完全不期望的,因为特定的 MyException
catch
块永远不会执行。
您会经常发现 .NET Base Class Library 中的许多派生异常类并未向 System::Exception
类添加任何功能。这是因为这些派生类仅用于隔离特定异常,而不是实际向 Exception
类添加任何额外的方法或属性。假设我们要打开一个文件,然后向其中写入一些信息。现在,假设打开文件或向已打开的文件写入时可能出现异常情况,我们显然需要以不同的方式处理它们,如下面稍微有些牵强的代码片段所示。
public __gc class FileOpenException : public Exception
{
};
public __gc class FileWriteException : public Exception
{
};
try
{
//allocate buffer and fill it
//open file (might throw FileOpenException)
while( condition )
{
//write buffer to file (might throw FileWriteException)
}
}
catch ( FileOpenException* foe )
{
//show file open error message
}
catch ( FileWriteException* fwd )
{
//close file (because it's currently open)
//delete file (as it's not been written into properly)
//show file write error message
}
__finally
{
//deallocate buffer
}
抛出 __value 对象
假设出于某种原因,我们想要一个自定义异常对象,它是一个 __value
class
或 __value struct
(显然不是派生自 System::Exception
)
public __value class MyValueClass
{
public:
int u;
};
如果我们尝试抛出该类的对象,我们会收到以下编译器错误消息(为防止滚动,已分成两行):
c:\...\ExceptionsDemo.cpp(107): error C2715: 'MyValueClass' :
unable to throw or catch an interior __gc pointer or a value type
所以我们需要在抛出 __value
对象之前对其进行装箱,如下所示:
void Test()
{
MyValueClass m;
m.u = 66;
//throw m; (won't compile)
throw __box(m);
}
我们可以将其作为 System::Object
类型的异常捕获,然后对其进行拆箱以访问实际的异常对象。
void TryTest()
{
try
{
Test();
}
catch(Object* m)
{
MyValueClass v = *dynamic_cast<__box MyValueClass*>(m);
Show(v.u);
}
}
__try_cast 和 System::InvalidCastException
虽然这与托管异常的一般性讨论不直接相关,但我还是想在这里提及 __try_cast
,它是 C++ 托管扩展支持的一个附加关键字。该强制转换运算符的工作方式与 dynamic_cast
运算符完全相同,只是如果强制转换失败,它会抛出一个 System::InvalidCastException
异常。
void TestCast()
{
try
{
Object* obj = new Object();
String* str = __try_cast<String*>(obj);
}
catch(InvalidCastException* ice)
{
Show(ice->Message);
}
}
结论
我希望这篇文章能为来自原生 C++ 背景、正在逐渐转向 .NET 和托管 C++ 的程序员阐明 VC++.NET 中的托管异常处理。有关 .NET 异常处理的更多信息,我推荐 Tom Archer 和 Andrew Whitechapel 的 Inside C# (第 2 版)[^] - 特别是第 12 章(第 411 - 448 页)。
历史
- 2003 年 8 月 9 日 - 首次发布