C++修炼:异常
Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!
我的博客:<但凡.
我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C++修炼之路》
欢迎点赞,关注!
异常可以说是一种非常厉害的错误处理方式,不仅C++在用,java,python等也都在用。那么这期我们就深入学习一下异常。
目录
1、异常的概念
1.1、什么是异常
1.2、异常的抛出与捕获
1.3、查找匹配的处理代码
基类与派生类异常定义
异常触发函数
异常捕获场景
关键点说明
扩展测试场景
1.4、异常重新抛出
1.5、异常安全问题
1.6、异常规范
2、标准库的异常
标准异常类
使用示例
自定义异常
异常处理建议
1、异常的概念
1.1、什么是异常
在C语言中,我们主要是通过错误码的形式处理错误。也就是说对各种错误进行编号。在C++中我们不用这么麻烦的方法去处理错误了,我们出现错误时,也就是异常时直接抛出一个对象,这个对象可以包含各种错误信息。
1.2、异常的抛出与捕获
当出现错误是,我们通过throw这个关键字来抛出一个异常对象,同时呢我们可以用catch关键字进行捕获。try
也是异常处理机制的关键字,他一般和catch搭配使用。
话不多说我们直接上代码:
#include<iostream>
using namespace std;
int division(int x, int y)
{if (y == 0){string s("Division by zero!");throw(s);}else{return x / y;}return 0;
}
int func1(int x,int y)
{try{cout << division(x, y) << endl;}catch (const char* ch){cout << ch << endl;cout << "Successful catch!" << endl;}return 0;
}
int func2(int x, int y)
{try{func1(x,y);}catch (string& s){cout << s << endl;cout << "Successful catch!" << endl;}return 0;
}
int main()
{func2(10, 5);func2(10, 0);return 0;
}
在上面这个逻辑中,当除数为0是会发生错误,此时应该会抛出异常。
抛出异常后,会暂定当前程序,开始寻常与之匹配的catch子句。对于try catch语句,如果没有捕获到异常,就会继续在外层调用函数链中查找,也就是返回上一层函数。一直查找,直到异常被捕获。那么这个查找的过程就叫做栈展开。
如果某个异常一直没有被捕获,当他找不到任何能够和他匹配的捕捉时就会报错终止程序。
终止程序是通过标准库函数terminate来实现的。
上述的栈展开的过程都是在编译时进行的。
下面我们拿func2(10,0)来举例看看栈展开的过程是什么样的。
当捕获之后,后续的代码正常执行。比如说我们上面的代码在func2中被捕获了,那么继续执行func2中catch之后的代码,也就是return 0。
一个try可以匹配多个catch:
try{cout << division(x, y) << endl;}catch (const char* ch){cout << ch << endl;cout << "Successful catch!" << endl;}catch (string& s){cout << s << endl;cout << "Successful catch!" << endl;}
当有多个catch与异常匹配的时候,距离抛出异常最近的catch优先捕获。 并且还有两点需要注意:1、沿着调用链中的其他函数可能提前退出。2、一旦程序开始处理异常,沿着调用链创建的对象都将销毁。
抛出异常对象后,会生成一个异常对象的拷贝,因为异常对象可能是局部对象。这个拷贝对象在catch子句之后销毁。
1.3、查找匹配的处理代码
一般情况下catch是和抛出对象完全匹配的。但是也有例外,捕获允许权限缩小(常量向非常量转换)、数组向指向数组元素类型的指针,函数向指向函数的指针,但最重要的是允许派生类向基类转换。
我们在捕获异常时,可以设置一个基类,再设置几个典型的异常去继承这个基类,那么对于捕获来说,我们直接捕获基类,这个基类的所有子类就都被捕获了。如果我们不需要捕获这个基类的异常,可以直接把这个基类定义成抽象类。
以下是一个展示C++异常捕获子类的经典场景示例,通过基类异常和派生类异常的分层处理,体现异常捕获的多态性和优先级机制。
基类与派生类异常定义
#include <iostream>
#include <stdexcept>// 基类异常
class BaseException : public std::runtime_error {
public:BaseException(const std::string& msg) : std::runtime_error(msg) {}
};// 派生类异常
class DerivedException : public BaseException {
public:DerivedException(const std::string& msg) : BaseException(msg) {}
};
异常触发函数
void simulateError(int code) {if (code == 1) {throw DerivedException("Derived Exception Occurred");} else if (code == 2) {throw BaseException("Base Exception Occurred");} else {throw std::runtime_error("Generic Error");}
}
异常捕获场景
int main() {try {simulateError(1); // 触发派生类异常} catch (const DerivedException& e) {std::cerr << "Caught DerivedException: " << e.what() << std::endl;}catch (const BaseException& e) {std::cerr << "Caught BaseException: " << e.what() << std::endl;}catch (const std::exception& e) {std::cerr << "Caught Generic Exception: " << e.what() << std::endl;}return 0;
}
关键点说明
- 捕获顺序:派生类异常(
DerivedException
)的catch块必须放在基类(BaseException
)之前,否则派生类异常会被基类catch块截获。 - 多态性:所有异常类型最终继承自
std::exception
,可通过基类引用捕获任意派生类异常。 - 输出示例
当调用simulateError(1)
时,输出结果为:Caught DerivedException: Derived Exception Occurred
扩展测试场景
修改main()
中的simulateError
参数,测试不同异常类型:
simulateError(2); // 触发基类异常
simulateError(3); // 触发通用异常
我们为了防止到main函数中无法捕获异常而终止程序,一般会在main函数中使用catch(...)捕获,可以捕捉到任意类型的异常。但是我们无法判断异常错误是什么。
1.4、异常重新抛出
捕获异常后,可以直接在 catch 块中重新抛出该异常。
我们在后面介绍标准库的异常之后再实现一下异常重新抛出的场景。
1.5、异常安全问题
在异常抛出之后,当前程序之后的代码就不会执行了。倘若在抛出异常之前向内存申请了一些资源,这部分资源不会被释放,此时会造成内存泄漏。我们可以通过智能指针来解决这个问题,我们下一期会介绍智能指针。其实智能指针就是让每个申请的内存在出作用域时自己释放资源(通过析构函数实现)。
#include <iostream>
using namespace std;void test()
{int* arr1 = new int[10];int* arr2 = new int[10];int* arr3 = new int[10];throw 1;delete[] arr1;//内存泄漏delete[] arr2;delete[] arr3;
}int main() {try{test();}catch (...){cout << "successful catch" << endl;}return 0;
}
在上面这个场景中会造成内存泄漏。而且不仅如此,还有一个更严重的问题,就是new本身也会抛异常。但是如果我们对于每次new都进行异常捕获并且重新抛出的话,对于连续多个new的话写起来太麻烦了。这个也是可以使用智能指针来解决。
另外就是析构函数中对抛异常也要进行谨慎处理,否则也可以会导致资源释放不完全而内存泄漏。
1.6、异常规范
在C++11之后如果某个函数不会抛异常,我们只需要在函数列表之后加noexcpt。但是抛异常的函数也可以不加noexcept,并不会报错。但是如果某个函数标明了noexcept,但是却抛出了异常,编译器会调用terminate终止程序,因为他会认为这个异常没有被捕获:
#include <iostream>
using namespace std;void test()noexcept//程序终止
{int* arr1 = new int[10];int* arr2 = new int[10];int* arr3 = new int[10];throw 1;delete[] arr1;//内存泄漏delete[] arr2;delete[] arr3;
}int main() {try{test();}catch (...){cout << "successful catch" << endl;}return 0;
}
还可以用noexcept()来判断一个表达式是否会抛异常。可能抛异常返回false,否则返回true。
#include <iostream>
using namespace std;void test()//程序终止
{int* arr1 = new int[10];int* arr2 = new int[10];int* arr3 = new int[10];throw 1;delete[] arr1;//内存泄漏delete[] arr2;delete[] arr3;
}int main() {cout << noexcept(test()) << endl;//输出0return 0;
}
需要注意的是,上面的代码中noexcept只是检测test会不会抛出异常,但是并没有真正执行test函数,所以也不会终止程序。
2、标准库的异常
C++标准库中的异常主要分为两类:标准异常类和自定义异常类。标准异常类均继承自std::exception
,提供统一的错误处理接口。
标准异常类
-
逻辑错误(
std::logic_error
)
表示程序逻辑错误,通常在编写代码时可避免。常见子类:std::invalid_argument
:参数无效std::out_of_range
:超出有效范围std::length_error
:超出容器最大长度
-
运行时错误(
std::runtime_error
)
表示运行时无法避免的错误。常见子类:std::overflow_error
:算术溢出std::underflow_error
:算术下溢std::system_error
:系统调用错误
-
其他异常
std::bad_alloc
:内存分配失败(如new
操作)std::bad_cast
:动态类型转换失败(dynamic_cast
)
使用示例
#include <iostream>
#include <stdexcept>
#include <vector>int main() {try {std::vector<int> v(5);v.at(10) = 1; // 抛出 std::out_of_range} catch (const std::out_of_range& e) {std::cerr << "Error: " << e.what() << std::endl;} catch (const std::exception& e) {std::cerr << "General error: " << e.what() << std::endl;}
}
自定义异常
继承std::exception
或其子类可自定义异常:
class MyException : public std::runtime_error {
public:MyException(const std::string& msg) : std::runtime_error(msg) {}
};try {throw MyException("Custom error");
} catch (const MyException& e) {std::cerr << e.what() << std::endl;
}
接下来我们补一下之前提到的异常重新抛出场景:
#include <iostream>
#include <stdexcept>void riskyOperation() {throw std::runtime_error("Original error occurred");
}void intermediateFunction() {try {riskyOperation();}catch (...) {std::cout << "Logging the error before rethrow" << std::endl;throw; // 重新抛出}
}int main() {try {intermediateFunction();}catch (const std::exception& e) {std::cout << "Caught in main: " << e.what() << std::endl;}
}
当然在实践中我们一般是捕获到特定类型的异常再重新抛出。以上只是一个最基本的异常重新抛出场景。
好了,今天的内容就分享到这,我们下期再见!