【C++:异常】C++ 异常讲解指南:从理论到实践,深入理解栈展开和优雅处理程序错误

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《算法题讲解指南》--优选算法
《算法题讲解指南》--递归、搜索与回溯算法
《算法题讲解指南》--动态规划算法

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

[一. 异常的核心概念与基本语法](#一. 异常的核心概念与基本语法)

1、异常的核心概念

2、异常的核心思想

3、基础语法格式和简单示例

基本语法格式:

简单示例(除零异常):

[二. 异常的核心机制:栈展开与匹配规则](#二. 异常的核心机制:栈展开与匹配规则)

1、栈展开

2、异常捕获的匹配规则

[三. 自定义异常体系:大型项目的最佳实践](#三. 自定义异常体系:大型项目的最佳实践)

[四. 异常的高级用法](#四. 异常的高级用法)

1、异常重新抛出

示例:网络请求重试

2、异常安全:避免资源泄漏

[2.1 注意事项](#2.1 注意事项)

[2.2 解决方案](#2.2 解决方案)

[2.3 示例演示](#2.3 示例演示)

[3、异常规范( noexcept )](#3、异常规范( noexcept ))

[五. C++ 标准库异常体系](#五. C++ 标准库异常体系)

结束语


前言

在之前 C语言 的学习中,我们通过错误码处理异常,但错误码只能返回简单状态,无法携带详细错误信息,且需要手动逐层检查,繁琐且易遗漏。C++ 的异常机制则彻底改变了这一现状 ------ 它将**"错误检测" 与 "错误处理" 进行分离** ,允许程序在出错时抛出异常对象 (携带完整错误信息),在另外合适的位置捕获并处理,则无需出现错误立马进行错误处理的情况,让代码更优雅、逻辑更清晰。

一. 异常的核心概念与基本语法

1、异常的核心概念

  • 异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的一部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分,检测环节无需知道问题的处理模块的所有细节
  • C语言主要通过错误码 的形式处理错误,错误码的本质就是对错误信息进行分类编号 ,拿到错误码以后还要去查询错误信息,比较麻烦。而异常是抛出一个对象,这个对象可以函数更全面的拿到各种信息。

2、异常的核心思想

  • 抛出(throw):程序遇到错误时,通过 throw 抛出一个异常对象(可是任意类型,推荐自定义异常类);
  • 捕获(catch):通过 catch 语句捕获指定类型的异常,执行对应的处理逻辑;
  • try 块 :try 包裹可能抛出异常的代码,后续紧跟一个或多个 catch 块,用于匹配不同类型的异常

分析

  • 程序出现问题时,我们通过抛出(throw)一个对象来引发一个异常,该对象的类型以及当前调用链决定了应该由那个catch的处理代码来处理该异常。
  • 被选中的处理代码是调用链中与该对象类型匹配且抛出异常位置最近的那一个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误。
  • throw 执行 时,throw 后面的语句将不再被执行。程序的执行从 throw 位置跳到与之匹配的 catch 模块,catch 可能是同一函数中的一个局部的 catch,也可能是调用链中另一个函数的 catch,控制权从 throw 位置转移到了 catch 位置。
  • 抛出异常对象后,会生成一个异常对象的拷贝 ,因为抛出的异常对象可能是一个局部对象 ,所以会生成一个拷贝对象,这个拷贝的对象会在 catch 子句后销毁 。( 这里的处理类似于函数的传值返回)

3、基础语法格式和简单示例

基本语法格式

cpp 复制代码
//抛异常基本语法格式:
/*-----------------------------------------------------------------
try 
{
    // 可能抛出异常的代码
    可能出错的函数();
} 
catch (异常类型1& e) 
{
    // 处理类型1异常
} 
catch (异常类型2& e) 
{
    // 处理类型2异常
}
catch (...) 
{
    // 捕获任意类型异常(兜底处理)
}
-----------------------------------------------------------------*/

简单示例(除零异常):

cpp 复制代码
//===================================简单示例(除零异常)===================================
#include<exception>
double Divide(int a, int b)
{
    // 当 b == 0 时抛出异常
    if (b == 0)
    {
        //string s("Divide by zero condition!");
        //throw s;

        throw exception("Divide by zero condition!");
    }
    else
    {
        return ((double)a / (double)b);
    }
}

void Func()
{
    try
    {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (const exception& e)
    {
        cout << e.what() << endl;
        cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
    }
    //类型匹配时就会遵循"就近原则"
    
}

int main()
{
    while(1)
    {
        try
        {
            Func();
        }
        // 异常会先匹配最适配的
        catch (const string& s)
        {
            cout << s << endl;
        }
        catch (const exception& e)//由于在上面已经进行了捕获处理,则这里不会再进行检测
        {
            cout << e.what() << endl;
        }
        catch (...) // 任意类型的对象(兜底处理)
        {
            cout << "未知异常" << endl;
        }
    }
    return 0;
}

二. 异常的核心机制:栈展开与匹配规则

1、栈展开

抛出异常后,程序会暂停当前函数执行,沿调用链向上查找匹配的 catch 块,这个过程称为 "栈展开":

  • 抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理。
  • 如果当前函数中没有try/catch子句,或者有try/catch子句但是类型不匹配,则会先销毁当前函数的局部对象,再退出当前函数,继续在外层调用函数链中查找。上述查找的catch过程被称为栈展开。
  • 重复上述两个步骤,直到找到匹配的catch;
  • 如果到达main函数,依旧没有找到匹配的catch子句,程序会调用标准库的terminate 函数强制终止程序;
  • 如果找到匹配的catch子句处理后,catch子句后面的代码会继续执行

栈展开示例:

cpp 复制代码
//===================================栈展开===================================
void Func1() 
{
    throw "Func1抛出异常"; // 抛出异常
}

void Func2() 
{
    try
    {
        Func1(); // 调用Func1,捕获时无法匹配类型,不处理,退出当前函数栈进行查找
    }
    catch (const int* errmsg)
    {
        // 捕获Func1抛出的异常(栈展开:Func1→Func2→Func3→main)
        cout << "捕获异常:" << errmsg << endl;
    } 
}

void Func3()
{
    try
    {
        Func2(); // 调用Func2,捕获时无法匹配类型,不处理,退出当前函数栈进行查找
    }
    catch (const double* errmsg)
    {
        // 捕获Func1抛出的异常(栈展开:Func1→Func2→Func3→main)
        cout << "捕获异常:" << errmsg << endl;
    }
    ////在查找过程如果中发现调用函数链其中某个函数的catch语句可以匹配,则进行处理(就近原则)
    ////处理完后调用函数链后续的catch语句则不再进行检查,也就是下面的mian函数中catch语句虽然匹配也不会再捕获。
    //catch (const char* errmsg) 
    //{
    //    // 捕获Func1抛出的异常(栈展开:Func1→Func2→Func3→main)
    //    cout << "捕获异常:" << errmsg << endl;
    //}
}

int main() 
{
    try 
    {
        Func3(); // 调用Func3
    }
    catch (const char* errmsg)
    {
        // 捕获Func1抛出的异常(栈展开:Func1→Func2→Func3→main)
        //若调用函数链其中某个函数的catch语句匹配成功(假设Func2)则在当前函数进行处理,
        //后续catch则不再进行检查(Func3和main)
        cout << "捕获异常:" << errmsg << endl;
    }
    return 0;
}

2、异常捕获的匹配规则

捕获异常时,遵循**"精确匹配优先、兼容转换次之"**的原则:

  • 优先匹配与抛出对象类型完全一致的catch;
  • 支持有限的类型转换
    • 非常量→常量( int→const int );
    • 数组→数组元素指针( int[ 5 ]→int* );
    • 派生类→基类(最实用,用于自定义异常体系);
  • 若有多个catch块,按顺序匹配 ,匹配成功不再检查后续catch
  • catch (...) 可捕获任意类型异常 ,通常作为兜底避免程序强制终止

补充

三. 自定义异常体系:大型项目的最佳实践

在大型项目中,直接抛出基本类型(如字符串、整数)的异常难以区分错误类型,推荐自定义异常类体系(基于继承 ),统一异常接口,便于管理和扩展。

核心思路:定义一个基类 Exception,派生类对应不同模块的异常 (如 SQL 异常、缓存异常、HTTP 异常),通过多态返回详细错误信息。

代码实现:

cpp 复制代码
//================================= 自定义异常体系 =================================
#include<thread>
// 一般大型项目程序才会使用异常,下面我们模拟设计一个服务的几个模块
// 每个模块的继承都是Expection的派生类,每个模块可以添加自己的数据
// 最后捕获的时候,通过派生类到基类的类型转换,我们统一捕获基类的引用即可, 通过多态可以打印不同信息

class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{
	}

	virtual string what() const
	{
		return _errmsg;
	}

	int getid()const
	{
		return _id;
	}

protected:
	string _errmsg;
	int _id;
};

class SqlException :public Exception
{
public:
	SqlException(const string& errmsg, int id, const string& sql)
		:Exception(errmsg, id)
		, _sql(sql)
	{
	}

	virtual string what() const
	{
		string str = "SqlException:";
		str += _errmsg;
		str += "->";
		str += _sql;
		return str;
	}
private:
	const string _sql;
};

class CacheException : public Exception
{
public:
	CacheException(const string& errmsg, int id)
		:Exception(errmsg, id)
	{
	}

	virtual string what() const
	{
		string str = "CacheException:";
		str += _errmsg;
		return str;
	}
};

class HttpException : public Exception
{
public:
	HttpException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id)
		, _type(type)
	{
	}

	virtual string what() const
	{
		string str = "HttpException:";
		str += _type;
		str += ":";
		str += _errmsg;
		return str;
	}

private:
	const string _type;
};

void SQLMgr()
{
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}
	else
	{
		cout << "SQLMgr 调用成功" << endl;
	}
}

void CacheMgr()
{
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	}
	else
	{
		cout << "CacheMgr 调用成功" << endl;
	}
	SQLMgr();
}

void HttpServer()
{
	if (rand() % 3 == 0)
	{
		throw HttpException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpException("权限不足", 101, "post");
	}
	else
	{
		cout << "HttpServer调用成功" << endl;
	}
	CacheMgr();
}


int main()
{
	srand(time(0));
	int i = 10;
	while (i--)
	{
		this_thread::sleep_for(chrono::seconds(1));

		try
		{
			HttpServer();
		}
		catch (const Exception& e) // 这里捕获基类,基类对象和派生类对象都可以被捕获
		{
			// 多态调用
			//形成多态两个条件:
			//(1)派生类对象传给的是基类指针或者引用
			//(2)派生类对基类的虚函数进行了重写
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
		cout << "*******************************************" << endl;
	}
	return 0;
}

四. 异常的高级用法

1、异常重新抛出

有时捕获异常后,无法完全处理(如仅记录日志),或需要根据错误类型分流处理,可通过throw; 重新抛出异常 ,让外层调用链继续处理

示例:网络请求重试

cpp 复制代码
//================================= 异常重新抛出 =================================
// 下面程序模拟展示了聊天时发送消息,发送失败补货异常,
// 但可能只是在电梯地下室等场景手机信号不好,则需要多次尝试
// 如果多次尝试都发送不出去,则就需要捕获异常再重新抛出,
// 其次如果不是网络差导致的错误,捕获后也要重新抛出。

void _SendMsg(const string& s)
{
	if (rand() % 2 == 0)
	{
		throw HttpException("网络不稳定,发送失败", 102, "put");
	}
	else if (rand() % 7 == 0)
	{
		throw HttpException("你已经不是对方的好友,发送失败", 102, "put");
	}
	else
	{
		cout << "发送成功" << endl;
	}
}

// 网络不稳定,要求重试三次,均失败
void SendMsg(const string& s)
{
	for (size_t i = 0; i < 4; i++)
	{
		try
		{
			_SendMsg(s);

			// 走到这里,如果没有抛异常导致结束
			// 那就代表成功了,可以执行到这个break,跳出循环
			break;
		}
		catch (const Exception& e)//HttpException是Exception的派生类,所以HttpException抛异常会在这里进行捕获
		{
			if (e.getid() == 102)
			{
				if (i == 3)
					// 重新抛出异常
					// throw e;
					throw; //捕获的异常是什么,重抛的异常就是什么

				cout << "开始第" << i + 1 << "重试" << endl;
			}
			else
			{
				// 重新抛出异常
				// throw e;
				throw; //捕获的异常是什么,重抛的异常就是什么
			}
		}
	}
}

int main()
{
	srand(time(0));
	string str;
	while (cin >> str)
	{
		try
		{
			SendMsg(str);
		}
		catch (const Exception& e) //重抛的异常也是HttpException,所以会被Exception基类引用进行捕获
		{
			cout << e.what() << endl << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}
	return 0;
}

2、异常安全:避免资源泄漏

2.1 注意事项

  • 异常抛出后,后面的代码就不再执行,前面申请了资源(内存 、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。
  • 其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要进行捕获处理,否则后面的5个资源就没释放,也资源泄漏了。《EffctiveC++》中的第8个条款也专门讲了这个问题,别让异常逃离析构函数。

2.2 解决方案

  • 手动捕获释放:在 catch 中释放资源后再重新抛出异常;
  • RAII 机制 :利用类的构造 / 析构自动管理资源(推荐,如智能指针、自定义资源管理类)后面的博客中还会再详细讲的;
  • 析构函数不抛异常:析构函数若抛出异常,可能导致资源释放不完全,需在析构函数内部捕获处理。

2.3 示例演示

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错误抛出异常直接被mian函数捕获,那下面的array就没有得到释放。
	// 所以我们可以在这里先进行捕获,目的就是为了能释放array的内存,但我们并不会在这里处理异常
	// 所以当释放array了内存后我们再重新抛出异常,还是交给外层处理
	int* array = new int[10];

	int len, time;
	cin >> len >> time;
	try {
		cout << Divide(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete []" << array << endl;
		delete[] array;

		// 重新抛出,捕获到什么抛出什么
		//throw "Division by zero condition!";
		throw;
	}

	cout << "delete []" << array << endl;
	delete[] array;
}

int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (...)
	{
		cout << "Unkown Exception" << endl;
	}
	return 0;
}

3、异常规范( noexcept )

C++11 提供 noexcept 关键字,用于声明函数是否会抛出异常,帮助编译器优化代码:

  • 对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,知道某个函数是否会抛出异常有助于简化调用函数的代码。
  • C++98 中函数参数列表的后面接 throw()表示函数不抛异常 ,函数参数列表的后面接throw(类型1,类型2...) ,表示可能会抛出多种类型的异常(可能会抛出的类型用逗号分割)
  • C++98的方式这种方式过于复杂,实践中并不好用,C++11中进行了简化,函数参数列表后面加 noexcept 表示不会抛出异常,啥都不加表示可能会抛出异常
  • 编译器并不会在编译时检查noekcept,也就是说如果一个函数用noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是一个声明了noexcept的函数抛出了异常,程序会调用 terminate 强制终止程序。
  • noexcept(expression) 还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会则返回false,不会就返回true。

实际示例

cpp 复制代码
//================================= 异常规范(noexcept) =================================
// C++98用来标记会抛异常的方法(较为麻烦)
// double Divide(int a,int b) throw(const char*)
// C++11标记不会抛异常的方法(更加简洁)
// double Divide(int a, int b) noexcept

// C++98
// 这里表示这个函数只会抛出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
// size_type size() const noexcept;
// iterator begin() noexcept;
// const_iterator begin() const noexcept;

//如果在声明了noexcept的函数中抛出了异常,则程序会调用terminate 强制终止程序。
//这样是不好的,所以加上noexcept的函数我们一定要确保不会抛异常,不要乱加
double Divide(int a, int b)
{
    // 当b == 0时抛出异常
    if (b == 0)
    {
        //throw "Division by zero condition!";
    }
    return (double)a / (double)b;
}

void Func()
{ }

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;
    //这里需要注意一下:虽然Divide(1, 2)实际没有抛异常,但是noexcept返回结果仍然是0
    //原因在于noexcept检测一个表达式如果可能会存在抛异常的风险(风险就是函数后面没有加上noexcept),则就返回false
    cout << noexcept(Divide(1, 0)) << endl;
    cout << noexcept(++i) << endl;

    cout << noexcept(Func()) << endl;
    //甚至像Func()这种连throw都没有的函数,如果函数后面没有加上noexcept,也会视作可能存在抛异常的风险,返回false
    //也就是说只有当一个函数后面加上noexcept,noexcept这个运算符才能视为不会抛异常,返回true
    return 0;
}

五. C++ 标准库异常体系

C++ 标准库提供了一套预定义的异常继承体系 ,基类为std::exception,派生类 对应不同类型的标准异常(如内存分配失败、数组越界),可直接使用或继承扩展。

​标准库链接:std::exception

标准库异常体系核心类:

异常类 用途 错误信息获取方式
std::exception 所有标准异常的基类 what()(虚函数)
std::bad_alloc new分配内存失败时抛出 what() 返回 "bad alloc"
std::out_of_range 数组/容器越界时抛出 what() 返回越界信息
std::invalid_argument 无效参数时抛出 what() 返回参数错误信息

不过我们日常的话一般使用 std::exception 就足够了。

结束语

到此,C++的异常我们就讲解完了**。C++ 异常机制是大型项目错误处理的首选方案,它让错误处理逻辑与业务逻辑分离,代码更清晰、可维护。掌握异常的基本语法、栈展开机制、自定义异常体系和异常安全,能让你在应对复杂错误场景时游刃有余。实际开发中,建议结合 RAII 机制(如智能指针)解决资源泄漏问题,基于标准库std::exception扩展自定义异常。**希望对大家学习C++能有所收获!

C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/

相关推荐
莫等闲-2 小时前
代码随想录一刷记录Day31——leetcode56. 合并区间 738.单调递增的数字
数据结构·c++·算法·leetcode
qq_452396232 小时前
【工程实战】第八篇:报告美学 —— Allure 深度定制:让 Bug 定位精准到秒
开发语言·python·bug
Zqrnja2 小时前
PTA 2026天体选拔赛(多校联赛)L2-1 仪式网络(C++ 含代码解释)
开发语言·c++
llm大模型算法工程师weng3 小时前
负载均衡做什么?nginx是什么
运维·开发语言·nginx·负载均衡
逆境不可逃3 小时前
【后端新手谈13】VO、BO、PO、DO、DTO:Java 分层开发的 5 大核心数据对象
java·开发语言
古月方枘Fry3 小时前
三层交换+VRRP实现负载
开发语言·网络·php
qq_5470261793 小时前
Java 中的 Caffeine 缓存详解
java·开发语言·缓存
H Journey3 小时前
C++ 强制类型转换
c++·类型转换
froginwe113 小时前
JSP 发送邮件
开发语言