【037】从异常到优雅的错误处理:揭秘C++异常机制的神奇力量

引言


💡 作者简介:一个热爱分享高性能服务器后台开发知识的博主,目标是通过理论与代码实践的结合,让世界上看似难以掌握的技术变得易于理解与掌握。技能涵盖了多个领域,包括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(...)// 任何异常都捕获
{
	// 处理异常的代码
}
  1. 异常的抛出:
    使用"throw"语句可以在程序的某个地方抛出异常。"throw"语句后面可以跟一个异常对象,该对象可以是任意类型,通常从std::exception派生而来。

    例如,我们可以这样抛出一个整型异常:

    int numerator = 10;
    int denominator = 0;
    if (denominator == 0) {
        throw 100; // 抛出一个整型异常
    }
    
  2. 异常的捕获:
    使用"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)是指在异常处理过程中,当异常被抛出时,程序会自动回退栈上的所有对象,直到找到匹配的异常处理代码为止。栈解旋是异常处理机制的一部分,用于确保在异常发生时,栈上的对象能够正确地被销毁,资源能够正确地释放。

当程序抛出异常时,异常处理机制会根据调用栈中的信息,逐级回退栈上的对象。具体的步骤如下:

  1. 异常被抛出并开始向上层调用栈传播。

  2. 当异常传播到某个函数时,函数内部的局部对象会按照创建的逆序依次被销毁。这包括自动变量、栈对象、对象的成员变量等。这个过程称为栈解旋。

  3. 如果在栈解旋的过程中找到匹配的异常处理代码,程序会跳转到对应的"catch"块,并执行相关的异常处理代码。

  4. 如果在栈解旋的过程中没有找到匹配的"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::failureio操作过程出现错误。
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++异常机制的总结:

  1. 抛出异常:当程序发生错误或异常情况时,可以使用throw语句抛出一个异常。异常可以是任何类型的对象,但通常是标准库定义的异常类的实例。

  2. 捕获异常:使用trycatch语句块来捕获和处理异常。在try块中放置可能抛出异常的代码,而在catch块中处理异常。catch块根据捕获的异常类型来执行相应的处理逻辑。

  3. 异常处理顺序:当发生异常时,C++按照try块中代码的顺序查找适合的catch块来处理异常。如果找到相应的catch块,将执行其代码;如果没有找到匹配的catch块,异常将继续向上一层调用栈传播,直到找到匹配的catch块或程序终止。

  4. 异常类型:C++标准库提供了一系列的异常类,例如std::exceptionstd::logic_errorstd::runtime_error等。这些类继承自std::exception类,并提供了特定的异常类型来表示不同的错误或异常情况。

  5. 自定义异常:除了使用标准库提供的异常类外,也可以自定义异常类来表示特定的错误或异常情况。自定义异常类通常需要继承自标准库的异常类或其子类,并可以添加自定义成员和行为。

  6. 异常安全性:异常机制对于程序的异常安全性非常重要。通过正确处理异常,可以确保资源的正确释放和程序状态的一致性。

总的来说,C++的异常机制提供了一种有效的方式来处理程序中的错误和异常情况。它允许程序员在发生异常时捕获和处理异常,以及在需要时自定义异常类型。正确使用异常机制可以提高程序的健壮性和可维护性。

在这里插入图片描述

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion Long

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值