异常处理
1. 什么是异常
在编程的世界里,异常就像是一个突然出现的“小插曲”,告诉我们程序在某些地方遇到了问题。
在面向对象语言中,比如 C++,异常是处理错误的一种常用方式。想象一下,当一个函数发现自己无法处理的错误时,它可以像抛球一样“抛出”一个异常,让这个函数的上级(直接或间接的调用者)来处理这个错误。
而在面向过程的语言里,比如 C 语言,通常使用返回值来表示错误。
举个例子,假设你在写一个计算除法的函数,如果除数是0,那么这个函数就不能正常计算了。在 C 语言中,你可能通过返回一个特殊的值(比如 -1 )来表示错误。
但在 C++ 中,你可以抛出一个异常,告诉调用者:“嘿,出问题了,除数不能是 0 !”
2. 异常处理机制
C 语言异常处理机制
终止程序:这是最简单也最粗暴的方式,比如发生内存错误或除 0 错误时,程序就会直接终止。但这种方式用户很难接受,因为程序突然就没了。
返回错误码:这种方式需要程序员自己去查找对应的错误码。比如,很多系统库的接口函数都会把错误码放到一个叫做 errno 的变量中。
setjmp 和 longjmp 组合:这是 C 标准库提供的一种异常处理机制,但使用起来比较复杂,不适合小白。
C++ 的异常处理
C++的异常处理就像是一场“接球游戏”,包括两个部分:抛出异常和捕获异常。
抛出异常( throw ):当程序遇到问题时,可以使用throw 关键字抛出一个异常。比如:
if (divisor == 0) {
throw "Divisor cannot be zero!";
}
在函数声明中,你还可以指定可能抛出的异常类型。比如:
void show(); // 该函数可能抛出任何异常
void show() throw(); // 该函数不抛出任何异常
void show() throw(char, int); // 该函数可能抛出char 和 int 型异常
捕获异常( try...catch ):try 块中放置的是可能抛出异常的代码,而 catch 块则用来捕获和处理这些异常。比如:
try{
// 可能抛出异常的代码
int result = divide(10, 0);
} catch (const char* msg) {
// 捕获并处理char*类型的异常
std::cerr << "Error: " << msg << std::endl;
} catch (int errorCode) {
// 捕获并处理int类型的异常
std::cerr << "Error code: " << errorCode << std::endl;
}
如果异常被捕获并处理,程序会继续执行 try...catch 之后的代码。如果异常未被捕获,程序将会终止。
3. 异常安全
异常安全就像是我们编程时的“保险带”,它确保程序在抛出异常时不会崩溃或产生不可预知的行为。
构造函数:构造函数负责对象的构造和初始化。最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。想象一下,如果你买了一个新手机,但还没完全开机就坏了,那得多郁闷啊!
析构函数:析构函数负责对象资源的清理。最好不要在析构函数中抛出异常,否则可能导致资源泄露(比如内存泄露、文件句柄未关闭等)。就像是吃完饭后不洗碗,下次吃饭就没碗用了。
RAII:C++中经常使用 RAII(Resource Acquisition Is Initialization)的方式来解决资源泄露的问题。简单来说,就是在对象的构造函数中获取资源,在析构函数中释放资源。这样,即使抛出异常,资源也会被正确地释放。
4. 异常规范
为了让函数的使用者知道某个函数可能抛出哪些类型的异常,C++标准提供了一些规范。
列出异常类型:在函数的后面接 throw(type1, type2, ...),列出这个函数可能抛出的所有异常类型。比如:
void func() throw(A, B, C, D); // 表示func函数可能会抛出A/B/C/D类型的异常
不抛异常:在函数的后面接 throw() 或 noexcept(C++11及以后),表示该函数不会抛出异常。比如:
void* operator new(std::size_t size) throw(std::bad_alloc); // 表示这个函数只会抛出 bad_alloc 的异常
void* operator new(std::size_t size, void* ptr) throw(); // 表示这个函数不会抛出异常
默认情况:如果函数没有异常接口声明,那么该函数可以抛出任何类型的异常。
但请注意,异常接口声明并不是强制的。