引言
💡 作者简介:一个热爱分享高性能服务器后台开发知识的博主,目标是通过理论与代码实践的结合,让世界上看似难以掌握的技术变得易于理解与掌握。技能涵盖了多个领域,包括C/C++、Linux、Nginx、MySQL、Redis、fastdfs、kafka、Docker、TCP/IP、协程、DPDK等。
👉
🎖️ CSDN实力新星、CSDN博客专家
👉
🔔 专栏介绍:从零到c++精通的学习之路。内容包括C++基础编程、中级编程、高级编程;掌握各个知识点。
👉
🔔 专栏地址:C++从零开始到精通
👉
🔔 博客主页:https://blog.csdn.net/Long_xu
🔔 上一篇:【036】读懂C++的强制类型转换static_cast、const_cast、dynamic_cast以及reinterpret_cast
一、异常的基本概念
C++异常机制是一种处理程序运行时错误的机制。当程序在运行过程中发生异常情况,比如除零错误、内存访问错误、数组下标越界、读取的文件不存在、空指针、内存不足、访问非法内存或者其他不可预测的错误,程序可以抛出一个异常,然后通过异常处理机制来捕获和处理这个异常。遇到错误,抛出异常、捕获异常。
异常由"throw"语句抛出,可以是任意的数据类型,通常是派生自std::exception类的异常对象。异常对象可以携带有关异常的信息,比如错误的原因、位置等。异常是一个类。
异常的处理是通过"try-catch"语句块来实现的。"try"块用于包含可能抛出异常的代码,当异常发生时,程序会跳转到与之对应的"catch"块,并执行相关的异常处理代码。"catch"块用于捕获和处理特定类型的异常。
如果一个异常在"try"块中没有被捕获到,它会继续向上层调用栈传播,直到找到匹配的"catch"块或者到达程序的顶层,如果没有处理的"catch"块,程序会终止并引发一个未处理异常的错误。
在处理异常时,可以使用多个"catch"块来捕获不同类型的异常,并根据需要进行相应的处理。此外,还可以使用"finally"块来执行无论是否发生异常都必须执行的代码,比如资源的释放等。
C++异常机制提供了一种结构化的方式来处理程序运行时的错误,使得代码更加可读、可维护,并提供了一种灵活的错误处理机制。
C++异常机制相比C语言异常处理的优势:
- 函数的返回值可以忽略,但是异常不可以忽略。如果忽略异常将导致程序退出。
- 函数的返回值没有任何语义信息,而异常却包含语义信息。有时从类名就能够体现出来。
二、异常的抛出和捕获
C++异常的抛出和捕获是通过使用"throw"和"try-catch"语句来实现的。
语法:
try{
throw 异常值;
}
catch(异常类型1 异常值1)
{
// 处理异常的代码
}
catch(异常类型2 异常值2)
{
// 处理异常的代码
}
catch(...)// 任何异常都捕获
{
// 处理异常的代码
}
-
异常的抛出:
使用"throw"语句可以在程序的某个地方抛出异常。"throw"语句后面可以跟一个异常对象,该对象可以是任意类型,通常从std::exception派生而来。例如,我们可以这样抛出一个整型异常:
int numerator = 10; int denominator = 0; if (denominator == 0) { throw 100; // 抛出一个整型异常 }
-
异常的捕获:
使用"try-catch"语句可以捕获并处理异常。在"try"块中包含可能抛出异常的代码,当异常发生时,程序会跳转到与之对应的"catch"块,并执行相关的异常处理代码。例如,我们可以这样捕获并处理前面抛出的整型异常:
try { // 可能抛出异常的代码 int result = numerator / denominator; } catch (int e) { // 捕获整型异常 // 处理异常的代码 std::cout << "除零错误,异常代码为:" << e << std::endl; }
注意,"catch"块中的参数类型必须与抛出的异常类型匹配,否则该"catch"块不会捕获异常。
可以使用多个"catch"块来捕获不同类型的异常,并根据需要进行相应的处理。通常,将更具体的异常类型的"catch"块放在前面,将更一般的异常类型的"catch"块放在后面。如果没有找到匹配的"catch"块,则异常会继续向上层调用栈传播。
除了捕获特定类型的异常,还可以使用不带参数的"catch"块来捕获任意类型的异常,这样可以在处理异常时获取更多的信息。
try { // 可能抛出异常的代码 } catch (...) { // 捕获任意类型的异常 // 处理异常的代码 }
在"catch"块中,可以执行相应的异常处理代码,比如输出错误信息、进行日志记录、回滚操作等。
异常的捕获可以嵌套使用,即在一个"catch"块中再次使用"try-catch"语句来捕获更深层次的异常。
try { // 可能抛出异常的代码 } catch (int e) { try { // 进一步处理异常的代码 } catch (...) { // 捕获更深层次的异常并处理 } }
最后,如果异常在整个调用栈中没有被捕获到,程序会终止并引发一个未处理异常的错误。因此,在使用异常处理机制时,要确保所有可能的异常都能够被适当地捕获和处理。
示例:
#include <iostream>
using namespace std;
int main()
{
try {
//throw 1;
//throw 'c';
throw 3.14f;
}
catch(int e){
cout << "整型异常,e = " << e << endl;
}
catch (char e)
{
cout << "字符型异常,e = " << e << endl;
}
catch (...) {
cout << "其他异常" << endl;
}
return 0;
}
注意:如果抛出的异常不捕获或者捕获不到,系统会中断结束程序。
三、栈解旋
C++栈解旋(Stack Unwinding)是指在异常处理过程中,当异常被抛出时,程序会自动回退栈上的所有对象,直到找到匹配的异常处理代码为止。栈解旋是异常处理机制的一部分,用于确保在异常发生时,栈上的对象能够正确地被销毁,资源能够正确地释放。
当程序抛出异常时,异常处理机制会根据调用栈中的信息,逐级回退栈上的对象。具体的步骤如下:
-
异常被抛出并开始向上层调用栈传播。
-
当异常传播到某个函数时,函数内部的局部对象会按照创建的逆序依次被销毁。这包括自动变量、栈对象、对象的成员变量等。这个过程称为栈解旋。
-
如果在栈解旋的过程中找到匹配的异常处理代码,程序会跳转到对应的"catch"块,并执行相关的异常处理代码。
-
如果在栈解旋的过程中没有找到匹配的"catch"块,异常会继续传播到上一层的调用栈,直到找到匹配的"catch"块或者到达程序的顶层。
栈解旋是一个自动的过程,由编译器和运行时系统负责处理。它确保在异常发生时,栈上的对象能够正确地被销毁,避免资源泄漏和内存泄漏的问题。同时,栈解旋也提供了一种机制,使得异常处理代码能够被正确地执行,从而进行相应的错误处理。
需要注意的是,栈解旋的过程不会执行函数的析构函数,而是直接销毁对象,即对象的析构函数不会被调用。因此,在使用栈解旋时,应该确保局部对象的析构函数中不会有可能引发异常的操作,以免引发更严重的问题。
栈解旋是C++异常处理机制的一个重要环节,用于确保在异常发生时,栈上的对象能够正确地被销毁,资源能够正确地释放,同时提供了一种机制,使得异常处理代码能够被正确地执行。
示例:
#include <iostream>
using namespace std;
class Data {
private:
int data;
public:
Data(int num)
{
data = num;
cout << "Data构造函数 " << num << endl;
}
~Data()
{
cout << "Data析构函数 " << data << endl;
}
};
int main()
{
try {
Data ob1(100);
Data ob2(200);
Data ob3(300);
throw 3.14f;
}
catch(int e){
cout << "整型异常,e = " << e << endl;
}
catch (char e)
{
cout << "字符型异常,e = " << e << endl;
}
catch (...) {
cout << "其他异常" << endl;
}
return 0;
}
输出:
Data构造函数 100
Data构造函数 200
Data构造函数 300
Data析构函数 300
Data析构函数 200
Data析构函数 100
其他异常
通俗的讲,栈解旋就是异常抛出后,从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构,析构的顺序和构造的顺序相反,这一过程称为栈的解旋。
四、异常的接口声明
异常的接口声明是指可以抛出哪些类型的异常。
(1)函数默认可以抛出任何类型的异常(推荐)。示例:
void test()
{
throw 1;
throw 'c';
throw "hello";
}
(2)只能抛出特定类型异常;在函数右边使用throw(异常类型列表)
修饰。示例:
void test() throw(int,char)
{
throw 1;
throw 'c';
//throw "hello";//抛出,不能捕获。
}
(3)不能抛出任何异常;在函数右边使用throw()
修饰。
void test() throw()
{
//throw "hello";//不能抛出异常
//throw 1;
//throw 'c';
}
五、异常变量的生命周期
定义一个这样的异常变量类:
class MyException{
public:
MyException(){
cout<<"MyException异常变量构造函数"<<endl;
}
MyException(const MyException& e){
cout<<"MyException异常变量k拷贝构造函数"<<endl;
}
~MyException(){
cout<<"MyException异常变量析构函数"<<endl;
}
};
(1)普通对象接异常值。将发生拷贝构造。示例:
#include <iostream>
using namespace std;
class MyException {
public:
MyException() {
cout << "MyException异常变量构造函数" << endl;
}
MyException(const MyException& e) {
cout << "MyException异常变量拷贝构造函数" << endl;
}
~MyException() {
cout << "MyException异常变量析构函数" << endl;
}
};
void test()
{
try
{
throw MyException();
}
catch (MyException e)//普通对象接异常,发生拷贝构造
{
cout << "普通对象接异常" << endl;
}
}
int main()
{
test();
return 0;
}
输出:
MyException异常变量构造函数
MyException异常变量拷贝构造函数
普通对象接异常
MyException异常变量析构函数
MyException异常变量析构函数
(2)以对象指针接异常值。没有发生拷贝构造,只构造一次和析构一次,但是需要手动释放指针,如果忘记释放堆空间很容易造成内存泄漏。
#include <iostream>
using namespace std;
class MyException {
public:
MyException() {
cout << "MyException异常变量构造函数" << endl;
}
MyException(const MyException& e) {
cout << "MyException异常变量拷贝构造函数" << endl;
}
~MyException() {
cout << "MyException异常变量析构函数" << endl;
}
};
void test()
{
try
{
throw new MyException;
}
catch (MyException *e)//普通对象接异常,发生拷贝构造
{
cout << "对象指针接异常" << endl;
delete e;
}
}
int main()
{
test();
return 0;
}
输出:
MyException异常变量构造函数
对象指针接异常
MyException异常变量析构函数
(3)对象引用接异常值(推荐)。使用引用会对对象取别名,由编译器处理,扩展对象的生命周期,在处理完异常后,自动调用析构;这样既不用占用内存空间、没有发生拷贝构造的调用以及不需要人为的释放内存空间,所以非常的好用。
#include <iostream>
using namespace std;
class MyException {
public:
MyException() {
cout << "MyException异常变量构造函数" << endl;
}
MyException(const MyException& e) {
cout << "MyException异常变量拷贝构造函数" << endl;
}
~MyException() {
cout << "MyException异常变量析构函数" << endl;
}
};
void test()
{
try
{
throw MyException();
}
catch (MyException &e)//普通对象接异常,发生拷贝构造
{
cout << "对象引用接异常" << endl;
}
}
int main()
{
test();
return 0;
}
输出:
MyException异常变量构造函数
对象引用接异常
MyException异常变量析构函数
六、异常的多态
可以自定义一个基类,子类继承父类,通过父类获取所有子类的异常输出,这就是利用虚函数实现多态性。
#include <iostream>
using namespace std;
class BaseException {
public:
virtual void printError() = 0;
};
// 空指针异常
class NullPointerException :public BaseException {
public:
virtual void printError()
{
cout << "NullPointerException" << endl;
}
};
// 越界异常
class OutOfRangePointerException :public BaseException {
public:
virtual void printError()
{
cout << "OutOfRangePointerException" << endl;
}
};
void test()
{
// throw NullPointerException();
throw OutOfRangePointerException();
}
int main()
{
try
{
test();
}
catch (BaseException& e)// 父类引用,可以捕获该父类派生出的所有子类的异常
{
e.printError();
}
return 0;
}
七、C++标准异常
C++的所有异常都是由exception派生的;所有的异常捕获都是使用父类去捕获的。C++标准库异常继承层次图:
异常名称 | 描述 |
---|---|
exception | 所有标准异常类的父类 |
bad_alloc | 当operator new and operator new[],请求分配内存失败时 |
bad_exception | 这是个特殊的异常,如果函数的异常抛出列表里声明了badexception异常,当函数内部抛出了异常抛出列表中没有的异常,这时调用的unexpected函数中抛出异常,不论什么类型,都会被替换成为badexception类型。 |
bad_typeid | 使用typeid操作符,操作一个NULL指针,而该指针是带有虚函数的类,这时抛出这个异常。 |
bad_cast | 使用dynamic_cast转换引用失败的时候。 |
ios_base::failure | io操作过程出现错误。 |
logic_error | 逻辑错误,可以在运行前检测的错误。 |
runtime_error | 运行时错误,仅在运行时才能检测的错误 |
logic_error的子类:
子类 | 描述 |
---|---|
std::invalid_argument | 表示传递给函数的参数无效。 |
std::domain_error | 表示一个函数或方法在一个无效的数学域(domain)上被调用。 |
std::length_error | 表示一个操作尝试超出有效范围的长度。 |
std::out_of_range | 表示一个索引或下标超出有效范围。 |
std::overflow_error | 表示一个算术运算结果超出了所能表示的最大值。 |
std::underflow_error | 表示一个算术运算结果超出了所能表示的最小值。 |
runtime_error的子类:
子类 | 描述 |
---|---|
std::runtime_error | 表示一般的运行时错误。 |
std::range_error | 表示一个值超出了有效范围。 |
std::overflow_error | 表示一个算术运算结果超出了所能表示的最大值。 |
std::underflow_error | 表示一个算术运算结果超出了所能表示的最小值。 |
std::logic_error | 表示逻辑错误。通常不被直接使用作为子类,但可以用于表示未被其他子类覆盖的运行时错误。 |
std::bad_alloc | 表示内存分配失败。 |
std::bad_cast | 表示类型转换失败。 |
std::bad_typeid | 表示typeid运算符在多态类型中失败。 |
std::bad_exception | 表示在异常处理过程中发生了无法处理的异常。 |
示例:
#include<iostream>
using namespace std;
int main()
{
try
{
throw out_of_range("我越界啦!!!");
}
catch (exception &e)
{
// waht()存放的是异常信息,以char*形式。
cout << e.what() << endl;
}
return 0;
}
输出:
我越界啦!!!
八、编写自己的异常
当基本的异常无法满足我们自己的需求时,就需要自己编写异常;编写自己的异常类也是要基于标准异常的基类,而不是自己随便乱写。
#include <iostream>
using namespace std;
class NewException :public exception {
private:
string str;
public:
NewException() {}
NewException(string msg) {
str = msg;
}
// 重写父类的waht()
virtual const char *what() const throw()//防止父类在子类前抛出标准异常
{
return str.c_str();
}
~NewException() {}
};
int main()
{
try
{
throw NewException("这是自己的异常");
}
catch (exception &e)
{
// waht()存放的是异常信息,以char*形式。
cout << e.what() << endl;
}
return 0;
}
总结
C++的异常机制是一种用于处理程序中的错误和异常情况的机制。以下是对C++异常机制的总结:
-
抛出异常:当程序发生错误或异常情况时,可以使用
throw
语句抛出一个异常。异常可以是任何类型的对象,但通常是标准库定义的异常类的实例。 -
捕获异常:使用
try
和catch
语句块来捕获和处理异常。在try
块中放置可能抛出异常的代码,而在catch
块中处理异常。catch
块根据捕获的异常类型来执行相应的处理逻辑。 -
异常处理顺序:当发生异常时,C++按照
try
块中代码的顺序查找适合的catch
块来处理异常。如果找到相应的catch
块,将执行其代码;如果没有找到匹配的catch
块,异常将继续向上一层调用栈传播,直到找到匹配的catch
块或程序终止。 -
异常类型:C++标准库提供了一系列的异常类,例如
std::exception
、std::logic_error
和std::runtime_error
等。这些类继承自std::exception
类,并提供了特定的异常类型来表示不同的错误或异常情况。 -
自定义异常:除了使用标准库提供的异常类外,也可以自定义异常类来表示特定的错误或异常情况。自定义异常类通常需要继承自标准库的异常类或其子类,并可以添加自定义成员和行为。
-
异常安全性:异常机制对于程序的异常安全性非常重要。通过正确处理异常,可以确保资源的正确释放和程序状态的一致性。
总的来说,C++的异常机制提供了一种有效的方式来处理程序中的错误和异常情况。它允许程序员在发生异常时捕获和处理异常,以及在需要时自定义异常类型。正确使用异常机制可以提高程序的健壮性和可维护性。