前言
在C语言中处理程序错误的方式是
assert
和返回错误码,然而C++中处理错的方式是抛出异常。
异常
处理异常的结构
C++异常处理是一种错误处理机制,它允许在程序运行时出现错误时,将错误信息传递回调用栈,以便进行异常处理。C++异常处理机制主要包括以下几个关键字:
try
:用于标记一个代码块,这个代码块可能会抛出异常。catch
:用于捕获前面try代码块中抛出的异常,并进行相应的处理throw
:用于抛出一个异常对象。throw()
:用于声明一个函数不抛出任何异常,即函数保证不会出现异常。throws
:用于声明一个函数可能抛出特定类型的异常。
处理异常的流程
- 使用try关键字包围可能抛出异常的代码块。
- 在try代码块中,如果发生异常,会创建一个异常对象并抛出。
- 程序的控制流会跳转到最近的catch块,catch块中的参数类型应该与抛出的异常对象的类型相匹配。
- 在catch块中处理异常,可以选择重新抛出异常或处理完毕后继续执行后续代码
//throw异常
void test()
{
double a, b;
cin >> a >> b;
if(b == 0)
throw "Division by zero condition!";
else
{
cout << a / b << endl;
}
}
//try catch
int main()
{
try
{
test();
}
catch (const char* str)
{
cout << str << endl;
}
return 0;
}
异常的抛出和匹配原则
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
- 上面的代码用
int
类型和chat*
进行catch,进行捕获的优先匹配chat*
- 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
- 例如;
mian
函数调用func1
func1
函数调用函数test
函数,当test
函数抛出异常,在func1和mian
函数中同时捕捉异常,就会优先匹配func1
中捕捉。 - 编译器在函数栈帧中会逐层寻找捕获指令。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象, 所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似 于函数的传值返回)
catch(...)
可以捕获任意类型的异常,问题是不知道异常错误是什么。
- 如果需要捕获所有类型的异常,可以使用catch(…)作为最后的兜底。
- 以防止异常抛出没有进行捕获,导致程序崩毁或者异常。
- **实际中抛出和捕获的匹配原则有个例外,**并不都是类型完全匹配,可以抛出的派生类对象, 使用基类捕获,这个在实际中非常实用。
- 在一个项目中会分出具体的模块,服务器组,网络模块,数据库模块等等,一般会写出多态进行的异常的抛出。
异常的重新抛出
异常的重新抛出可以通过throw关键字实现。
- 当你在一个catch块中捕获到一个异常后,如果需要将这个异常传递给外层的catch块处理,可以使用throw关键字重新抛出该异常。
void test()
{
double a, b;
int* c = new int[10];
cout << "new" << endl;
cin >> a >> b;
if(b == 0)
throw "Division by zero condition!";
else
{
cout << a / b << endl;
}
delete[] c;
cout << "delete" << endl;
}
int main()
{
try
{
test();
}
catch (const char* str)
{
cout << str << endl;
}
catch(...)
{
cout << "未知异常" << endl;
}
return 0;
}
- 上述代码进行异常抛出的时候没有进行,空间的释放,容易造成内存的泄露。
- 这样的额抛出无异于自杀式行为。
C++中提供了,异常的重新抛出。
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
int* array = new int[10];
try {
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
catch (...)
{
cout << "delete []" << array << endl;
delete[] array;
throw;
}
// ...
cout << "delete []" << array << endl;
delete[] array;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
return 0;
}
异常安全
-
构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不 完整或没有完全初始化
-
析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内 存泄漏、句柄未关闭等)
-
C++11引入了
noexcept
关键字,用于声明函数不会抛出异常。这为编译器提供了优化的机会,并向调用者提供了关于函数异常安全性的明确保证。如果一个noexcept
函数实际上抛出了异常,程序将调用std::terminate
来终止执行
异常规范
- 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的
- 函数的后面接throw(),表示函数不抛异常。
- 若无异常接口声明,则此函数可以抛掷任何类型的异常。
- 在业务流程中,异常规范可能涉及到异常报告、异常分析、异常处理流程、责任分配、纠正措施等。例如,企业可能会建立一套标准操作程序(SOP)来处理生产异常、质量异常、客户投诉等,以确保问题能够被及时发现、分析并解决。
后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
- 异常规范的建立和遵循对于提高系统的健壮性、可靠性和安全性至关重要。它们有助于减少因异常处理不当而导致的数据丢失、服务中断或安全漏洞。此外,良好的异常规范还能够提高团队的协作效率,因为所有成员都清楚在异常发生时应该采取的行动
自定义异常体系
-
在设计异常自定义体系时,开发者会定义一个基类异常,作为所有自定义异常的根,并根据需要创建多个派生异常类来表示不同类型的错误。这些派生异常类可以包含更多的错误信息,如错误代码、详细描述等,以便于错误的诊断和处理。
-
自定义异常体系的优点包括提高代码的可读性和可维护性,使得错误处理更加灵活和专业。通过自定义异常,开发者可以在不同的业务模块中抛出特定的异常类型,并在上层代码中通过统一的异常处理逻辑来捕获和处理这些异常,从而实现异常的集中管理和处理
C++标准库的异常体系
C++标准库的异常体系是C++编程语言中处理错误和异常情况的一个关键部分。它通过一系列预定义的异常类来帮助程序员报告和处理运行时错误。这些异常类形成了一个层次结构,基于std::exception类,提供了一种统一的方式来处理不同类型的错误。
异常类层次结构
-
std::exception:
这是所有异常类的基类。它提供了一个what()成员函数,用于返回描述异常的字符串。 -
std::logic_error
:表示程序逻辑错误,如不正确的参数值或不合理的状态。它有多个子类,包括:std::domain_error:
参数超出定义域。std::invalid_argument:
函数接收到的参数无效。std::length_error:
容器长度超出其最大允许长度。std::out_of_range:
索引或键不在容器的有效范围内。std::runtime_error
:表示运行时错误,如文件不存在或硬件故障。它通常用于捕获非预期的运行时错误。
-
std::bad_alloc:
表示内存分配失败,通常在new操作失败时抛出。 -
std::bad_cast:
表示在动态类型转换时失败,如dynamic_cast失败。 -
std::bad_typeid
:表示类型信息错误,如在typeid操作中出现错误。 -
std::bad_exception
:表示无法创建或获取异常对象,用于处理std::exception的派生类。 -
std::bad_function_call:
表示对没有绑定的函数对象的调用,如对未初始化的std::function的调用。 -
std::bad_array_new_length
:表示数组长度为负数或大于std::max_size_t。 -
std::bad_weak_ptr:
表示使用无效的std::weak_ptr。
异常的优缺点
异常处理机制在编程语言中,如C++,是一种强大的工具,用于处理程序运行时可能遇到的错误和非正常情况。它提供了一种结构化的方法来处理错误,而不是让程序在遇到错误时直接崩溃。然而,像任何工具一样,异常处理也有其优点和缺点。
异常的优点
- 错误处理的一致性:异常提供了一种统一的错误处理方式,使得代码更易于理解和维护。所有的错误处理都可以通过try、catch和throw语句来实现。
- 非正常流程的分离:异常使得正常的程序流程与错误处理流程分离,提高了代码的可读性和可维护性。正常逻辑不受错误处理代码的干扰。
- 堆栈回溯:当异常被抛出时,C++会自动进行堆栈回溯,找到最近的catch块,这有助于定位错误发生的准确位置。
- 资源清理:异常机制可以与RAII(Resource Acquisition Is Initialization)技术结合使用,确保资源在异常发生时被正确清理。
- 异常安全:通过设计异常安全的代码,可以确保在异常抛出时程序状态的完整性,避免数据的不一致性。
异常的缺点
- 性能开销:异常处理机制在抛出和捕获异常时存在一定的性能开销。频繁使用异常处理可能会影响程序的运行速度。
- 代码复杂性:虽然异常处理提高了代码的可读性和维护性,但过多的异常处理代码也可能增加代码的复杂性,尤其是当异常处理逻辑分散在多个catch块中时。
- 调试难度:异常的堆栈回溯虽然有助于定位错误,但在复杂的程序中,异常的传播路径可能变得难以跟踪,增加了调试的难度。
- 异常规范:在C++中,异常规范(如C++98和C++03中的throw规范和C++11中的noexcept)可能会引入额外的复杂性,尤其是在理解异常安全保证时。
- 滥用异常:将异常用于控制流,而不是真正的错误处理,是一种常见的滥用,可能会导致代码难以理解和维护。
总结
是当异常处理逻辑分散在多个catch块中时。
- 调试难度:异常的堆栈回溯虽然有助于定位错误,但在复杂的程序中,异常的传播路径可能变得难以跟踪,增加了调试的难度。
- 异常规范:在C++中,异常规范(如C++98和C++03中的throw规范和C++11中的noexcept)可能会引入额外的复杂性,尤其是在理解异常安全保证时。
- 滥用异常:将异常用于控制流,而不是真正的错误处理,是一种常见的滥用,可能会导致代码难以理解和维护。
总结
异常处理是一种强大的错误处理机制,它提高了代码的健壮性和可维护性。然而,它也带来了一定的复杂性和性能开销。因此,程序员应该明智地使用异常,避免滥用,并在性能敏感的代码中考虑替代的错误处理策略。在设计和实现代码时,应充分考虑异常的优缺点,以达到最佳的程序设计和性能平衡。