【C++】C++异常

🎬 个人主页MSTcheng · CSDN
🌱 代码仓库MSTcheng · Gitee
🔥 精选专栏 : 《C语言
数据结构
《算法学习》
C++由浅入深

💬座右铭: 路虽远行则将至,事虽难做则必成!


在前面的文章中,我们已经介绍了C++11的一些新特性。本文将和下一篇一起为大家讲解C++的最后两个重要主题:异常处理和智能指针。

文章目录

一、异常的概念及使用

1.1异常的概念

异常(Exception)是指在程序执行过程中发生的意外或错误情况,这些情况可能导致程序无法继续正常执行。异常处理是编程中用于管理这些意外情况的机制,旨在提高程序的健壮性和用户体验。
相比于C语言,C语言主要通过错误码的方式处理错误,而错误码的本质就是对错误的信息进行分类编写。拿到错误码以后还要去查询错误信息,是比较麻烦的。而异常时直接抛出一个对象,这个对象可以涵盖更全面的错误信息。

1.2异常的分类

1、编译时异常Checked Exception

  • 这类异常在编译阶段就会被检查,必须显式处理(捕获或声明抛出)。
    常见于外部资源操作,如文件不存在(FileNotFoundException)、数据库连接 失败等。

2、运行时异常Runtime Exception

  • 编译时不会被强制检查,通常由逻辑错误引发,如空指针访问NullPointerException)、数组越界(ArrayIndexOutOfBoundsException)等。

3、错误Error

  • 指严重问题 (如内存耗尽OutOfMemoryError),通常无法通过程序处理,需从系统层面解决。

1.3异常的抛出与捕获

异常的抛出:

  • 当程序出现问题的时候,首先通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前调用链来决定匹配哪个catch ,然后再被这个catch接收并处理异常。

异常的捕获:

  • 异常的捕获首先通过一个try/catch语句来捕获,并且该catch要与throw对象的类型匹配且为距离抛出异常位置最近的那一个catch,然后根据抛出的对象的类型和内容告知异常部分到底发生了什么错误。

下面我们就来看看C++的异常抛出与捕获机制:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;

//=================================
//Divide这个函数是用来计算两个数相除的函数
//对于一个除数来说被除数是不能为0的 也就是分子可以为0 但分母不能
//所以我们针对被除数是否为0设计了一个异常机制
//假设a为除数b为被除数 如果检测到b为0那么就抛出异常
//=================================
double Divide(int a, int b)
{
	try
	{
		// 当b == 0时抛出异常 引发除零错误
		if (b == 0)
		{
			//===============================
			//抛出异常对象后,会⽣成⼀个异常对象的拷⻉,
			//因为抛出的异常对象可能是⼀个局部对象,
			//所以会⽣成⼀个拷⻉对象,
			//这个拷⻉的对象会在catch⼦句后销毁。
			//(这⾥的处理类似于函数的传值返回)
			//===============================
			string s("Divide by zero condition!");
			throw s; //抛出的是一个string对象 catch的时候要用string类型接收
			
		}
		else
		{
			return ((double)a / (double)b);
		}

		//... fxx()
	}
	catch (const int& s)
	{
		cout << s << endl;
	}

	//第一个catch 这个catch的类型与抛出异常的string对象类型匹配 且离抛出位置最近
	/*catch (const string& errmsg)
	{
		cout << errmsg << endl;
	}*/

	cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
	return 0;
}

void Func()
{
	int len, time;
	cin >> len >> time;
	cout << Divide(len, time) << endl;
	cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}


int main()
{

	try
	{
		Func();
	}

	//第二个catch 虽然也是string类型 与抛出异常的string对象类型配但是离抛出位置较远
	//所以第一个catch存在的情况下程序优先会跳到第一个catch中
	catch (const string& errmsg)
	{
		cout << errmsg << endl;
	}

	catch (int errid)
	{
		cout << errid << endl;
	}

}

1、当被第一个catch捕获时

2、注释掉第一个catch,当被第二个catch捕获时

注意事项:

  1. throw执行时,throw后面的语句将不再执行,直接跳转到对应的catch中去执行catch可能是同一个函数中的局部catch,也可能是调用链中另一个函数中的catch,制空权从throw位置转移到了catch位置之后还有两个重要含义:
    1、沿着调用链的函数可能提早退出
    2、一旦程序开始执行异常处理程序,沿着调用链创建的对象都将销毁。

对于第一点含义我们对比两次catch执行的结果就会发现,当第一个catch捕获时,由于第一个catch与抛出异常的对象位于同一个域,所以当异常对象s被第一个catch捕获时并没有跳过func函数所以程序运行时会执行func函数中的内容而第二次catch就不同了 ,第二个catchmain函数中,throw之后直接跳过func函数,到main函数中执行catch之后的内容,跳过了func函数所以func函数中的内容不被执行。

对于第二点,我们就要来看看栈的展开了

1.4栈展开

  • 当抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句首先检查throw本⾝是否在try块内部 ,如果在则查找匹配的catch语句:如果有匹配的,则跳到catch的地方进行处理;如果当前函数中没有try/catch子句 ,或者try/catch子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开。 下面来看看栈展开图

  • 如果一直到main函数都没有找到了与之类型匹配的catch子句,则程序会调用标准库的terminate函数终止程序,如下图:

1.5 查找匹配的处理代码

⼀般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个。
但是也有⼀些例外,允许从非常量向常量的类型转换,也就是权限缩小允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针允许从派⽣类向基类类型的转换,这个点非常实⽤,实际中继承体系基本都是用这个方式式设计的。下面来看一些例子:

cpp 复制代码
//=============第一种==============
//精确匹配
try {
    throw 42;  // 抛出int类型
} 
catch (int e) {  // 精确匹配int
    std::cout << "Caught int: " << e;
}

//=============第二种===============
//权限缩小转换(非常量->常量)
try {
    char* ptr = new char[10];
    throw ptr;  // 抛出char*
} 
catch (const char* e) {  // 允许非常量转常量
    std::cout << "Caught const pointer";
    delete[] e;
}

//=============第三种==============
//派生类->基类转换
class Base 
{ 
	virtual void foo() {} 
};

class Derived : public Base 
{};

try {
    throw Derived();  // 抛出派生类对象
} 
catch (Base& e) {  // 捕获基类引用(多态处理)
    std::cout << "Caught Base reference";
}

//==============第四种=============
//数组->指针转换
try {
    int arr[5]{1,2,3};
    throw arr;  // 抛出int[5]
} 
catch (int* e) {  // 自动转为指针
    std::cout << "Caught pointer to first element: " << e[0];
}

//==============第五种=============
//就近原则匹配catch
try {
    throw std::string("error");
} 
catch (const std::string& e) {  // 优先匹配更近的
    std::cout << "Caught by string ref";
} 
catch (const std::exception& e) {
    std::cout << "Caught by exception ref";
}

另外:如果到main函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以⼀般main函数中最后都会使****⽤catch(...),它可以捕获任意类型的异常,但是是不知道异常错误是什么。

cpp 复制代码
#include <iostream>
#include <stdexcept>

int main() {
    try {
        // 可能抛出异常的代码
        throw std::runtime_error("An error occurred");
    }
    catch (const std::exception& e) 
    {
        std::cerr << "Caught exception: " << e.what() <<std::endl;
    }
    catch (...) //使用三个点来接收异常
    {
        std::cerr << "Caught an unknown exception" << std::endl;
    }
    return 0;
}

1.6异常重新抛出

有时catch到⼀个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对象直接抛出

cpp 复制代码
try 
{
    // 可能抛出异常的代码
    someRiskyOperation();
} 
catch (const std::exception& e) {
    if (isSpecialError(e)) 
    {
        // 特殊错误处理
        handleSpecialCase();
    } 
    else 
    {
        // 其他错误重新抛出
        throw;  // 注意没有参数
    }
}

注意throwthrow e的区别:

  1. throw重新抛出当前异常对象,不进行拷贝构造。
  2. throw e会通过拷贝构造函数创建一个新的异常对象。

1.7异常的安全问题

由于异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。中间我们需要捕获异常,释放资源后⾯再重新抛出。当然后面智能指针章节讲的RAII方式解决这种问题是更好的。

cpp 复制代码
double Divide(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 << Divide(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;
	}

	catch(const exception & e)
	{
		cout << e.what() << endl;
	} 

	catch(...)
	{
		cout << "Unkown Exception" << endl;
	} 
	return 0;
}

1.8异常规范

相比于传统C++98时的异常规范C++11中进行了简化:函数参数列表后面加
noexcept表示不会抛出异常,什么都不加表示可能会抛出异常。

  • 编译器并不会在编译时检查noexcept,也就是说如果⼀个函数用noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是⼀个声明了noexcept的函数抛出了异常,程序会调用terminate函数来终止程序。
  • 另外noexcept还可以作为一个运算符去检测一个表达式是否会抛出异常,如果会抛出异常就返回false,如果不会就返回true
cpp 复制代码
double Divide(int a, int b) noexcept
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	} 
	return (double)a / (double)b;
} 
int main()
{
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	} 
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	} 
	catch (...)
	{
		cout << "Unkown Exception" << endl;
	} 
	int i = 0;
	cout << noexcept(Divide(1,2)) << endl;
	cout << noexcept(Divide(1,0)) << endl;
	cout << noexcept(++i) << endl;
	return 0;
}

二、总结

C++的异常处理机制是管理运行时错误的重要工具,它通过try、catchthrow三个关键字的配合使用,能够有效应对程序执行中的意外情况。这套机制不仅确保了程序的健壮性,还能实现错误的优雅处理。因此,在代码编写时应当重视异常处理的应用。

html 复制代码
MSTcheng 始终坚持用直观图解 + 实战代码,把复杂技术拆解得明明白白!
👁️ 【关注】 看普通程序员如何用实用派思路搞定复杂需求
👍 【点赞】 给 "不搞虚的" 技术分享多份认可
🔖 【收藏】 把这些 "好用又好懂" 的干货技巧存进你的知识库
💬 【评论】 来唠唠 ------ 你踩过最 "离谱" 的技术坑是啥?
🔄 【转发】把实用技术干货分享给身边有需要的程序员伙伴
技术从无唯一解,让我们一起用最接地气的方式,写出最扎实的代码! 🚀💻

感谢能够看到这里的小伙伴,如果这篇文章有帮到您,还请给个三连!你们的持续支持是我更新最大的动力!谢谢!

相关推荐
草莓熊Lotso8 小时前
Linux 文件描述符与重定向实战:从原理到 minishell 实现
android·linux·运维·服务器·数据库·c++·人工智能
大模型玩家七七8 小时前
基于语义切分 vs 基于结构切分的实际差异
java·开发语言·数据库·安全·batch
历程里程碑8 小时前
Linux22 文件系统
linux·运维·c语言·开发语言·数据结构·c++·算法
岳麓丹枫0019 小时前
PostgreSQL 中 pg_wal 目录里的 .ready .done .history 文件的生命周期
数据库·postgresql
寻星探路13 小时前
【深度长文】万字攻克网络原理:从 HTTP 报文解构到 HTTPS 终极加密逻辑
java·开发语言·网络·python·http·ai·https
陌上丨16 小时前
Redis的Key和Value的设计原则有哪些?
数据库·redis·缓存
曹牧16 小时前
Spring Boot:如何测试Java Controller中的POST请求?
java·开发语言
在路上看风景16 小时前
19. 成员初始化列表和初始化对象
c++