[C++] 异常


文章目录
异常的概念
异常机制是一种重要的错误处理方法,可以帮助程序在运行时检测并处理问题,从而提高程序的可靠性和可维护性。C++异常机制的核心思想是:将错误检测和错误处理分离,从而让程序结构更清晰。
- 异常的作用
- 检测环节只需发现问题,而不需要关注问题的处理细节。
- 异常机制允许程序的某个部分通过抛出(
throw)信号,将错误信息传递给能够处理它的另一个部分。 - 异常对象比传统的错误码更灵活,因为它可以包含更丰富的上下文信息。
- C语言 vs. C++异常机制
- C语言通过返回错误码的方式处理错误,开发者需要检查返回值或查询对应的错误信息表,操作繁琐且易遗漏。
- C++通过异常对象,可以直接携带错误信息,程序员无需额外查找错误码。

异常的抛出与捕获
在C++中,异常的抛出和捕获分为以下几个步骤:
- 抛出异常 (
throw)
当程序遇到错误或特殊情况时,使用throw关键字抛出异常对象:
if (b == 0) {
string s("Divide by zero condition!");
throw s;
}
- 抛出的对象可以是内置类型、标准库类型(如
std::string)或用户自定义类型。 - 注意:
throw之后的代码不会被执行。
- 捕获异常 (
catch)
捕获异常通过try-catch块完成:
try {
cout << Divide(len, time) << endl;
} catch (const string& errmsg) {
cout << errmsg << endl;
}
try块包裹可能发生异常的代码。catch子句处理捕获的异常,参数类型必须匹配抛出异常的类型。- 捕获顺序:越具体的异常类型越靠前。例如,
catch (const std::exception& e)放在通用的catch (...)之前。
- 重要规则
- 如果
try块中没有匹配的catch子句,异常会沿调用链向上传递。 - 如果最终仍未找到匹配的
catch,程序会调用std::terminate()终止。

栈展开(Stack Unwinding)
栈展开是C++异常机制的核心,它描述了异常从抛出到被捕获的整个传播过程。
- 栈展开的流程
- 当异常被抛出时,程序会暂停当前函数的执行,并沿调用链查找匹配的
catch块。 - 首先检查
throw语句所在函数是否有try-catch,如果没有或类型不匹配,退出当前函数。 - 依次回退到调用函数,重复上述过程,直到找到匹配的
catch块或到达main函数。
- 对象销毁
- 栈展开过程中,函数局部对象会按逆序调用析构函数,释放资源。
- 这使得RAII(Resource Acquisition Is Initialization)在异常处理期间依然可靠。
- 未捕获异常
- 如果到达
main函数仍未找到匹配的catch块,程序会终止。

- 示例代码
double Divide(int a, int b) {
if (b == 0) {
string s("Divide by zero condition!");
throw s;
}
return (double)a / (double)b;
}
void Func() {
int len, time;
cin >> len >> time;
try {
cout << Divide(len, time) << endl;
} catch (const string& errmsg) {
cout << "Caught exception: " << errmsg << endl;
}
}
int main() {
try {
Func();
} catch (const string& errmsg) {
cout << "Unhandled exception: " << errmsg << endl;
}
return 0;
}
四、总结
- 异常机制的优势
- 提高代码的可读性和可维护性。
- 将错误检测与处理解耦,增强模块化设计。
- 支持复杂对象的生命周期管理(如RAII)。
- 开发建议
- 只在异常场景中使用异常,避免过度使用。
- 异常处理应尽量精准,不要捕获所有异常(如
catch (...))。 - 保证栈展开期间资源正确释放,推荐使用智能指针(如
std::shared_ptr、std::unique_ptr)。

查找匹配的处理代码
在C++的异常处理机制中,当程序抛出一个异常对象时,系统会按照一定规则查找与该对象类型匹配的catch代码块,并执行相应的异常处理逻辑。
- 完全匹配的优先规则
一般情况下,抛出的异常对象的类型与catch的形参类型完全匹配时,会优先选中该catch子句。例如:
try {
throw std::string("Error occurred");
} catch (const std::string& err) {
std::cout << "String exception: " << err << std::endl;
}
- 特殊匹配规则
如果没有完全匹配的catch块,C++允许以下类型转换来匹配:- 非常量向常量转换:允许从非
const类型转换为const类型。 - 数组与指针转换:允许数组转换为指向其元素类型的指针。
- 函数与指针转换:允许函数转换为指向函数的指针。
- 派生类向基类转换:这是面向对象编程中最常用的设计方式。在捕获派生类对象时,可以通过基类类型进行匹配。
- 非常量向常量转换:允许从非
- 继承体系下的匹配示例
继承体系允许捕获基类类型的异常,从而简化代码编写。例如:
class Exception {
public:
Exception(const std::string& errmsg) : _errmsg(errmsg) {}
virtual std::string what() const { return _errmsg; }
private:
std::string _errmsg;
};
class SqlException : public Exception {
public:
SqlException(const std::string& errmsg) : Exception(errmsg) {}
};
try {
throw SqlException("SQL Error");
} catch (const Exception& e) { // 基类捕获派生类异常
std::cout << e.what() << std::endl;
}
- 捕获通配符异常
如果异常没有与任何具体类型匹配,可以使用通配符catch (...)捕获所有类型的异常。这种方式一般用于处理未知异常:
try {
throw 42;
} catch (...) {
std::cout << "Unknown exception caught!" << std::endl;
}
- 未捕获异常的处理
如果异常传播到main函数仍未被捕获,程序会调用std::terminate()函数终止程序。为了避免程序非预期终止,可以在main中使用catch (...)捕获所有未匹配的异常。

异常的重新抛出
在某些情况下,捕获到一个异常后,需要将其重新抛出,供调用链上的其他部分继续处理。
- 重新抛出异常 (
throw;)
在catch块中,使用不带参数的throw关键字可以重新抛出当前捕获的异常。例如:
try
{
throw std::runtime_error("Error occurred");
}
catch (const std::exception& e) {
std::cout << "Caught: " << e.what() << ", rethrowing..." << std::endl;
throw; // 重新抛出异常
}
- 传递异常的场景
- 局部处理与传递:在捕获异常后执行部分处理操作,再重新抛出异常让更高层次代码进行处理。
- 日志记录:记录异常日志,然后将异常重新抛出。
- 重新抛出后的异常处理
重新抛出的异常会沿调用链继续传播,直至找到匹配的catch块。例如:
void InnerFunc() {
throw std::runtime_error("Inner exception");
}
void OuterFunc() {
try {
InnerFunc();
} catch (...) {
std::cout << "Logging exception in OuterFunc" << std::endl;
throw; // 重新抛出异常
}
}
int main() {
try {
OuterFunc();
} catch (const std::exception& e) {
std::cout << "Caught in main: " << e.what() << std::endl;
}
}
输出结果:
Logging exception in OuterFunc
Caught in main: Inner exception
- 带参数的重新抛出
可以在catch块中捕获异常后,抛出另一个异常对象:
try {
throw std::runtime_error("Original Error");
} catch (const std::exception& e) {
throw std::logic_error("New Error"); // 抛出新的异常
}
- 注意事项
- 在重新抛出异常时,资源的释放需要特别注意,建议使用智能指针或RAII管理资源。
- 捕获基类对象重新抛出时,避免丢失原始的派生类信息。
三、模拟示例:服务模块中的异常处理
以下示例展示了如何在复杂项目中使用异常处理、基类匹配以及重新抛出异常。
class Exception {
public:
Exception(const std::string& errmsg) : _errmsg(errmsg) {}
virtual std::string what() const { return _errmsg; }
private:
std::string _errmsg;
};
class SqlException : public Exception {
public:
SqlException(const std::string& errmsg) : Exception(errmsg) {}
};
void SQLMgr() {
throw SqlException("SQL Error");
}
void CacheMgr() {
try {
SQLMgr();
} catch (const Exception& e) {
std::cout << "Caught in CacheMgr: " << e.what() << std::endl;
throw; // 重新抛出
}
}
int main() {
try {
CacheMgr();
} catch (const Exception& e) {
std::cout << "Caught in main: " << e.what() << std::endl;
}
return 0;
}
运行结果:
Caught in CacheMgr: SQL Error
Caught in main: SQL Error
四、总结
- 查找匹配代码的关键点
- 完全匹配优先。
- 支持类型转换,如派生类向基类的转换。
- 提供通配符捕获(
catch (...))以处理未知异常。
- 异常重新抛出
- 使用
throw;重新抛出当前异常。 - 可以抛出不同的异常对象,向上传递更多的上下文信息。
- 重新抛出时要注意资源管理,避免内存泄漏。
- 使用
通过合理运用异常匹配与重新抛出,能够让程序在复杂情况下保持健壮性和可维护性。

C++ 异常规范详解
在C++中,异常规范是描述函数是否可能抛出异常,以及可能抛出哪些类型的异常。随着C++标准的演变,异常规范的用法发生了一些变化,从C++98的throw()到C++11及之后的noexcept,逐步变得简化和实用。
一、C++98异常规范
- 语法
在C++98中,函数的参数列表后面可以添加throw()或throw(类型列表),用于说明函数可能抛出异常的情况:
void func1() throw(); // 表示函数不会抛出任何异常
void func2() throw(std::bad_alloc); // 表示函数可能抛出std::bad_alloc异常
void func3() throw(int, char); // 表示函数可能抛出int或char类型的异常
- 问题
- C++98异常规范不会被强制执行。即便一个函数声明为
throw()(不会抛出异常),但实际抛出了异常,程序仍可能崩溃。 - 限制过于繁琐,在实践中难以使用。例如,声明多个可能抛出的类型时,类型检查复杂。
- C++98异常规范不会被强制执行。即便一个函数声明为
- 缺点
- 性能影响:编译器需要生成额外代码进行类型检查。
- 实际不可靠:标准库函数通常不使用
throw(类型),在现代开发中也很少被使用。
二、C++11及其后的异常规范 (noexcept)
为解决C++98中异常规范的不足,C++11引入了noexcept,替代throw(),并提供更强大的功能和简单的语法。
noexcept基本语法noexcept表示函数不会抛出任何异常:
void func1() noexcept; // 保证函数不会抛出异常
void func2(); // 未声明noexcept,可能抛出异常
- 如果`noexcept`函数实际抛出了异常,程序会调用`std::terminate()`终止执行,而不会进行异常传播。
noexcept(expression)
示例:
int i = 0;
std::cout << noexcept(++i) << std::endl; // 输出1(不会抛异常)
std::cout << noexcept(throw "Error!"); // 输出0(会抛异常)
- `noexcept`还可以作为**运算符**,用于判断表达式是否可能抛出异常:
noexcept(expression)
* 如果`expression`在编译期确定不会抛出异常,`noexcept(expression)`返回`true`。
* 如果可能抛出异常,返回`false`。
- 与C++98的区别
- 兼容性:
noexcept取代了throw(),现代C++中几乎不再使用throw()。 - 强制性:
noexcept是更强的约束,声明为noexcept的函数如果抛出异常,程序直接终止。 - 简单性:
noexcept比C++98的throw(类型)更简洁,无需列出具体类型。
- 兼容性:
- 编译器行为
- 不会强制检查:编译器不会在编译时检查
noexcept修饰的函数是否实际可能抛出异常。 - 运行时行为:如果
noexcept函数实际抛出了异常,直接调用std::terminate()。
- 不会强制检查:编译器不会在编译时检查
三、使用noexcept的场景与注意事项
- 标准库中的
noexcept
标准库中的许多函数使用了noexcept修饰。例如:
size_t size() const noexcept; // 容器的size()函数不会抛出异常
iterator begin() noexcept; // begin()函数也不会抛出异常
- 用户定义函数
- 如果可以明确保证函数不会抛出异常,建议使用
noexcept,这可以帮助编译器优化代码。 - 例如:
- 如果可以明确保证函数不会抛出异常,建议使用
double Divide(int a, int b) noexcept {
if (b == 0) {
throw "Division by zero condition!"; // 会导致std::terminate()
}
return (double)a / (double)b;
}
- 异常的影响
- 如果
noexcept函数抛出异常,程序终止执行,且不会传播异常。 - 因此,在设计API时,应当慎重决定是否使用
noexcept,只有在可以完全保证不抛出异常时才使用。
- 如果
- 优化潜力
- 编译器可以针对
noexcept函数进行优化,因为可以假设它们不会抛出异常。 - 对于容器操作(如
std::vector的移动构造),如果被移动的对象的移动操作声明为noexcept,容器可以更高效地移动对象。
- 编译器可以针对
四、综合示例
以下代码展示了noexcept的使用,以及noexcept(expression)运算符的行为:
#include <iostream>
#include <stdexcept>
int SafeDivide(int a, int b) noexcept {
if (b == 0) {
throw "Division by zero!"; // noexcept函数抛异常会终止程序
}
return a / b;
}
int PotentialThrow(int x) {
if (x < 0) throw std::runtime_error("Negative value!");
return x;
}
int main() {
try {
std::cout << "SafeDivide: " << SafeDivide(10, 0) << std::endl;
} catch (...) {
std::cout << "Caught exception in SafeDivide!" << std::endl;
}
std::cout << "noexcept(SafeDivide(10, 2)): "
<< noexcept(SafeDivide(10, 2)) << std::endl; // 输出1(静态分析)
std::cout << "noexcept(PotentialThrow(10)): "
<< noexcept(PotentialThrow(10)) << std::endl; // 输出0(可能抛异常)
return 0;
}
运行结果:
Caught exception in SafeDivide!
noexcept(SafeDivide(10, 2)): 1
noexcept(PotentialThrow(10)): 0
五、总结
- C++98中的异常规范(
throw()):- 提供对可能抛出的异常类型的声明,但在实践中不常用,现代C++中已基本弃用。
- C++11及之后的异常规范(
noexcept):- 简洁高效,标记函数不会抛出异常。
- 编译器可利用
noexcept进行优化,增强程序的性能。
- 实践建议:
- 对于不会抛出异常的函数,明确声明为
noexcept。 - 避免滥用
noexcept,因为一旦函数抛出异常,程序会直接终止。 - 使用
noexcept(expression)进行静态分析,确保表达式的安全性。
- 对于不会抛出异常的函数,明确声明为
通过合理使用异常规范,可以提高代码的可读性和可靠性,同时优化程序性能。


本文来自博客园,作者:DevKevin,转载请注明原文链接:https://www.cnblogs.com/kevinbee/p/18678205

浙公网安备 33010602011771号