目录
一、异常
二、C语言中对错误的处理
三、C++中的异常处理
四、异常的抛出和捕获
五、异常的重新抛出
六、C++标准库中的异常体系
七、异常的规范
一、异常
在C++中,异常是程序运行期间发生的意外或错误情况。这些情况可能会导致程序无法继续正常执行,但又不能在当前的代码位置立即处理。异常提供了一种机制,使得在错误发生时可以将控制权从当前代码的执行位置转移到异常处理代码的位置,从而进行适当的处理。
1.异常类型通常是类,可以是标准库中的异常类,也可以是自定义的异常类。异常类通常派生自
std::exception
类或其派生类。通过定义不同类型的异常类,可以让程序区分不同类型的错误情况,并采取相应的处理措施。2.当程序在执行过程中遇到错误情况时,可以使用
throw
关键字手动抛出异常。throw
后面通常跟着一个异常对象,可以是任何类型的对象,包括基本类型、类、指针等。3.异常捕获通过
try
和catch
块来实现。try
块用于包裹可能抛出异常的代码块,而catch
块用于捕获并处理在try
块中抛出的异常。catch
块可以捕获特定类型的异常或者所有类型的异常。4.异常处理是指在程序运行时遇到异常情况时所采取的行动。异常处理的目的是保证程序的稳定性和可靠性,使得程序能够在出错情况下进行适当的恢复或终止。
5.当一个函数中抛出了异常,但该函数没有处理这个异常时,异常将会被传递给调用该函数的上层函数,直到找到相应的异常处理代码为止。如果异常一直没有被处理,最终将导致程序的终止。
通过异常机制,C++ 提供了一种结构化的方式来处理程序中的错误情况,从而提高了程序的健壮性和可维护性。
二、C语言中对错误的处理
C语言中的处理机制:
1. 终止程序,如 assert ,缺陷:用户难以接受。如发生内存错误,除 0 错误时就会终止程序。2. 返回错误码 ,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno 中,表示错误
这两个方法存在不足之处
1. 终止程序,如assert
当程序遇到错误时,直接终止程序可能会给用户带来不好的体验,特别是对于用户交互型的程序而言。这种突然的终止也不利于程序的稳定性和可维护性。
2. 返回错误码
需要程序员在每次调用可能出错的函数后手动检查返回值,并查找对应的错误码,这增加了代码的复杂性和出错的可能性。此外,如果程序员忘记检查错误码,可能会导致错误被忽略或未处理,进而引发更严重的问题。同时,错误码通常是整数类型,可能不够精确地描述错误的性质和上下文,导致错误处理不够灵活。
三、C++中的异常处理
在C++中,通常使用三个关键字来处理:
throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。catch: 在您想要处理问题的地方,通过异常处理程序捕获异常 . catch 关键字用于捕获异常,可以有多个catch 进行捕获。try: try 块中的代码标识将被激活的特定异常 , 它后面通常跟着一个或多个 catch 块。
我们来看一下下面的例子(除0异常)
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
int main() {
int dividend = 10;
int divisor = 0;
float result;
try {
if (divisor == 0) {
throw "除数不能为零";
}
result = static_cast<float>(dividend) / divisor;
std::cout << "以下代码不会被执行" << std::endl;
std::cout << "结果: " << result << std::endl;
}
catch (const char* msg) {
std::cerr << "异常: " << msg << std::endl;
}
return 0;
}
可以看到一旦throw后,会直接跳转到对应的catch,后面的代码不会被执行。
四、异常的抛出和捕获
// 抛出对象决定匹配的 catch 块
void throwException(int value) {
if (value < 0) {
throw "遇到负值!";
}
else {
throw 42;
}
}
int main() {
try {
throwException(-1);
}
catch (const char* msg) {
std::cout << "捕获到带有消息的异常: " << msg << std::endl;
}
catch (int num) {
std::cout << "捕获到带有值的异常: " << num << std::endl;
}
return 0;
}
运行结果
#include <iostream>
// 匹配最近的 catch 块
void tryCatchChain(int option) {
try {
if (option == 1) {
throw "第一个";
}
else {
throw "第二个";
}
}
catch (const char* msg) {
std::cout << "在外部 catch 块中捕获到异常: " << msg << std::endl;
// throw; // 重新抛出异常
}
}
int main() {
try {
tryCatchChain(2);
}
catch (const char* msg) {
std::cout << "在主 catch 块中捕获到异常: " << msg << std::endl;
}
return 0;
}
运行结果可以看到最近的是外部捕获:
#include <iostream>
// 异常对象的拷贝和销毁
class MyException {
public:
MyException() {
std::cout << "异常对象已创建" << std::endl;
}
~MyException() {
std::cout << "异常对象已销毁" << std::endl;
}
};
void throwException() {
throw MyException();
}
int main() {
try {
throwException();
}
catch (MyException& e) {
std::cout << "捕获到异常" << std::endl;
}
return 0;
}
运行结果:
#include <iostream>
// catch(...) 的通配符
void throwException() {
throw "出现了问题!";
}
int main() {
try {
throwException();
}
catch (...) {
std::cout << "捕获到异常" << std::endl;
}
return 0;
}
运行结果:
#include <iostream>
// 使用基类捕获派生类对象
class BaseException {
public:
virtual void what() const {
std::cout << "基类异常" << std::endl;
}
};
class DerivedException : public BaseException {
public:
void what() const override {
std::cout << "派生类异常" << std::endl;
}
};
void throwException() {
throw DerivedException();
}
int main() {
try {
throwException();
}
catch (const BaseException& e) {
std::cout << "捕获到 ";
e.what();
}
return 0;
}
运行结果:
这个例子就发展到自定义异常体系
实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异常,那么外层的调用者基本就没办法玩了,所以实际中都会定义一套继承的规范体系。这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了
五、异常的重新抛出
#include<iostream>
using namespace std;
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
// 重新抛出去。
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;
}
Division
函数负责进行整数除法,但在除数为零时抛出异常"Division by zero condition!"
。
Func
函数中,我们使用new
分配了一个整数数组,然后在try
块中调用Division
函数,如果出现除以零的情况,会抛出异常。在
catch
块中,我们捕获了所有类型的异常catch (...)
。在这里,我们先输出一条消息表示我们正在释放动态分配的数组,然后使用delete[]
释放数组的内存。最后,在
catch
块中使用throw
重新抛出异常,这样异常会继续向外传递给上层调用者处理。在
main
函数中,我们捕获并处理const char*
类型的异常消息,然后输出错误消息。
catch
块会捕获异常并进行处理,比如在例子中,释放动态分配的数组。如果没有重新抛出,程序会继续执行 catch
块后的代码,而不是中断执行。这可能导致程序继续执行其他代码,导致不可预期的行为或错误,比如释放两次空间。
异常的重新抛出这种方式确保了在出现异常时,动态分配的资源得到正确释放,同时异常也被传递给上层处理。
异常安全问题:
构造函数完成对象的构造和初始化 , 最好不要 在构造函数中抛出异常,否则 可能导致对象不 完整或没有完全初始化析构函数主要完成资源的清理 , 最好不要 在析构函数内抛出异常,否则 可能导致资源泄漏 ( 内存泄漏、句柄未关闭等)C++ 中异常经常会导致资源泄漏的问题,比如在 new 和 delete 中抛出了异常,导致内存泄漏,在lock 和 unlock 之间抛出了异常导致死锁, C++ 经常使用 RAII 来解决以上问题。使用RAII来申请空间,就算直接跳到catch出了作用域也会自动调用析构来释放。
六、C++标准库中的异常体系
在 C++ 中,标准异常类都是以父子类层次结构组织的,它们的定义在 <stdexcept>
头文件中。这个层次结构的根是 std::exception
类,它是所有标准异常类的基类。其他的异常类都直接或间接地继承自 std::exception
,形成了一个继承体系。这样的设计使得异常处理更加方便,可以根据需要捕获特定类型的异常,也可以捕获更通用的异常。
下面是一些常见的标准异常类及其关系:
std::exception
:所有标准异常类的基类。
std::bad_alloc
:内存分配失败时抛出的异常。std::bad_cast
:在动态类型转换中出现问题时抛出的异常。std::bad_typeid
:当typeid
运算符应用于空指针时抛出的异常。std::logic_error
:所有逻辑错误异常的基类。
std::invalid_argument
:当函数参数无效时抛出的异常。std::domain_error
:当函数参数超出有效域时抛出的异常。std::length_error
:当试图创建超出长度限制的对象时抛出的异常。std::out_of_range
:当使用超出有效范围的值时抛出的异常。std::runtime_error
:所有运行时错误异常的基类。
std::range_error
:当执行超出范围的结果时抛出的异常。std::overflow_error
:当执行算术运算超出范围时抛出的异常。std::underflow_error
:当执行算术运算下溢时抛出的异常。
七、异常的规范
异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的 后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;
如果使用noexpect仍然抛出异常,编译会告警:
但是有的编译器就算加了也不一定会告警,所以使用的时候要注意。