深入理解 C++ 异常机制

发布于:2025-03-28 ⋅ 阅读:(25) ⋅ 点赞:(0)

前言

在 C++ 编程中,异常处理是一种强大的机制,能够帮助程序在运行时优雅地处理错误,避免崩溃,提高程序的健壮性和可维护性。相比 C 语言通过错误码进行错误处理的方式,C++ 的异常处理提供了更清晰、更结构化的错误处理方法。本文将详细介绍 C++ 异常的概念、异常的抛出与捕获、栈展开、异常匹配、异常的重新抛出、异常安全以及异常规范等内容。


1. 异常的概念

在 C++ 中,异常(Exception)是一种特殊的事件,当程序运行过程中发生错误时,程序可以抛出异常,并由相应的异常处理代码进行处理。

异常处理的核心思想

异常处理的核心思想是将问题的检测与问题的解决分开

  • 检测错误的代码 负责发现问题,并抛出异常。
  • 异常处理代码 负责捕获异常并执行相应的错误处理逻辑。

C 语言与 C++ 异常的区别

在 C 语言中,通常使用 错误码 进行错误处理:

  • 需要手动检查函数的返回值,以判断是否出现错误。
  • 错误码需要进行分类编号,程序需要额外的错误查询机制,处理逻辑复杂。

C++ 通过异常对象进行错误处理:

  • 直接抛出一个异常对象,其中可以包含完整的错误信息。
  • 由异常处理机制自动捕获异常,避免手动检查返回值,使代码更清晰。 

2. 异常的抛出与捕获

当程序检测到错误时,可以使用 throw 关键字抛出异常对象,并使用 catch 语句捕获异常进行处理。

异常的处理流程

  1. 发生错误时,程序会 throw 一个异常对象。
  2. 异常会沿着调用链向上传播,直到找到匹配的 catch 语句。
  3. 如果找到匹配的 catch,则执行其中的异常处理代码。
  4. 如果没有找到匹配的 catch,则程序会终止。

抛出异常的规则

  • throw 语句后面的代码不会被执行,程序会立即跳转到匹配的 catch 语句。
  • 异常对象会被复制一份,并在 catch 语句处理后销毁。
  • catch 语句必须与异常的类型匹配,否则异常会继续向上传播。

匹配规则

  • 严格匹配catch 语句的类型必须与 throw 语句的类型一致,或者是它的基类。
  • 基类匹配:如果 catch 语句的参数是基类对象,可以捕获派生类异常(多态)。
  • 通配符捕获:使用 catch (...) 语句,可以捕获所有类型的异常。

throw 语句后面的代码不会被执行,程序会立即跳转到匹配的 catch 语句。根据错误类型,会跳到main函数的catch语句。

#include <iostream>
#include <vector>
using namespace std;
double divide(int a, int b) {
	try {
		if (b == 0) {
			string error = "Divide by zero";
            throw error;
		}
		else
			return (double)a / b;
	}
    catch (int errid) {
		cout << errid << endl;
	}
    return 0;
}
void Func(){
	int len, time;
	cin >> len >> time;
		try
	{
		cout << divide(len, time) << endl;
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}
int main() {
	while (1) {
		try {
			Func();
		}
		catch (const string& errmsg) {
			cout << errmsg << endl;
		}
	}
	return 0;
}

下面j就是调用最近的Func函数的catch语句 

double divide(int a, int b) {
    if (b == 0) {
        throw string("Divide by zero"); // 直接抛出 std::string
    }
    return (double)a / b;
}

void Func() {
    int len, time;
    cin >> len >> time;
    try {
        cout << divide(len, time) << endl;
    }
    catch (const string& errmsg) { 
        cout << "Error in Func: " << errmsg << endl;
    }
    cout << __FUNCTION__ << ":" << __LINE__ << " 行执行" << endl;
}

int main() {
    while (true) {
        try {
            Func();
        }
        catch (const string& errmsg) { 
            cout << "Error in main: " << errmsg << endl;
        }
    }
    return 0;
}


3. 栈展开

栈展开(Stack Unwinding)指的是当异常发生时,程序会沿着函数调用栈回溯,查找匹配的 catch 语句的过程。

栈展开的过程

  1. 检查当前函数 是否有匹配的 catch 语句:
    • 如果有,跳转到 catch 语句处理异常。
    • 如果没有,退出当前函数,并在上层调用链中继续查找。
  2. 清理局部变量
    • 退出函数时,会调用当前作用域内的局部对象的析构函数,释放资源。
  3. 继续向上查找 catch
    • 如果找到了匹配的 catch,程序继续执行 catch 语句中的代码。
    • 如果一直找不到 catch,最终会到达 main 函数。
  4. 程序终止
    • 如果 main 函数也没有捕获异常,程序会调用 std::terminate() 终止执行。


4. 异常的匹配规则

当异常被抛出时,程序会寻找匹配的 catch 语句进行处理,匹配规则如下:

  • 完全匹配:抛出的异常类型和 catch 语句的参数类型完全一致。
  • 常量转换:允许从非常量常量转换(如 intconst int)。
  • 数组和指针转换:允许数组转换成指向数组元素类型的指针。
  • 派生类向基类转换:允许派生类异常被基类的 catch 语句捕获(多态)。
  • 通配符匹配catch (...) 可捕获所有异常,但无法识别异常类型。
#include<thread>

// ⼀般⼤型项⽬程序才会使⽤异常,下⾯我们模拟设计⼀个服务的⼏个模块
// 每个模块的继承都是Exception的派⽣类,每个模块可以添加⾃⼰的数据
// 最后捕获时,我们捕获基类就可以
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));
	while (1)
	{
		this_thread::sleep_for(chrono::seconds(1));
		try
			
		{
		HttpServer();
		}
			catch (const Exception& e) // 这⾥捕获基类,基类对象和派⽣类对象都可以被捕获
			
		{
		cout << e.what() << endl;
		}
			catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}
	return 0;
}


5. 异常的重新抛出

在某些情况下,一个 catch 语句可能需要部分处理异常,然后重新抛出它,让更外层的 catch 处理剩余部分。可以使用 throw; 重新抛出当前异常。

典型应用

  • 先记录错误日志,然后重新抛出异常,让外层代码进行更详细的处理。
#include <iostream>
using namespace std;

void func() {
    int* arr = new int[10]; // 申请内存
    try {
        throw runtime_error("Memory error"); // 模拟异常
    }
    catch (const exception& e) {
        cout << "Error in func: " << e.what() << endl;
        delete[] arr; // 释放资源
        throw; // 重新抛出异常
    }
}

int main() {
    try {
        func();
    }
    catch (const exception& e) {
        cout << "Main caught: " << e.what() << endl;
    }
    return 0;
}


6. 异常安全问题

异常处理的一个关键问题是如何确保资源不会因为异常而泄漏,例如:

  • 动态内存分配:如果在 new 之后异常被抛出,delete 可能不会被执行,导致内存泄漏。
  • 锁管理:如果在获取锁后抛出异常,而没有释放锁,则可能导致死锁。
  • 文件操作:打开文件后,异常可能导致文件未能正确关闭。

解决方案

  1. RAII(资源获取即初始化)

    • 资源在构造函数中分配,在析构函数中释放,确保异常安全。
    • 例如使用 智能指针(std::unique_ptrstd::shared_ptr 来管理内存。
  2. try-catch 处理资源释放

    • catch 语句中手动释放资源,然后重新抛出异常。
  3. 避免异常逃离析构函数

    • 析构函数不应抛出异常,否则会导致程序 std::terminate() 终止。
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;
}


7. 异常规范

C++ 提供了一些异常规范,用于声明函数是否会抛出异常。

C++98 的异常规范

  • throw():表示函数不会抛出任何异常。
  • throw(int, char, std::exception):表示函数可能抛出指定类型的异常。

问题:C++98 的异常规范很复杂,难以维护,并且编译器不会强制检查。

C++11 的 noexcept

  • noexcept 关键字替代了 throw(),表示函数不会抛出异常:
    void func() noexcept;
    
  • noexcept(expression):可以检测表达式是否可能抛出异常:
    bool canThrow = noexcept(func());
    
  • 如果 noexcept 修饰的函数抛出异常,程序会终止!

实践建议

  • 仅在确保函数不会抛出异常时使用 noexcept,否则可能导致程序异常终止。

总结

C++ 的异常处理机制提供了一种清晰、结构化的错误处理方式,避免了 C 语言繁琐的错误码处理。关键点如下:

  1. throw 抛出异常,catch 捕获异常,异常对象可以携带详细错误信息。
  2. 异常沿着调用链传播(栈展开),直到找到匹配的 catch,否则程序终止。
  3. 异常安全 是编写健壮代码的重要原则,应使用 RAII、智能指针 等机制避免资源泄漏。
  4. C++11 noexcept 规范化了异常处理,提高了代码的可维护性和优化能力。

掌握 C++ 异常处理,将有助于编写更加稳定和健壮的程序! 🚀