我们知道,C语言处理错误的方式:

  1. 返回错误码,缺陷:需要程序员自己查找错误信息。
  2. 终止程序(如assert),缺陷:用户难以接受。

一、 异常的概念

由于C语言中处理错误的方式不够理想和方便,C++推出了自己处理错误的方式——异常。当一个函数遇到无法处理的错误时,会抛出异常,让函数的调用者来处理。

二、 异常的定义

先看看以下代码:

void func()
{
	throw "error";
}

void test()
{
	try
	{
		func();
	}
	catch (const char* str)
	{
		cout << str << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
}
  • throw用于抛出异常,
  • try用于包围住会抛出异常的代码块
  • catch用于捕获抛出的异常

注意:

  1. try语句可以和多条catch语句连接,这样子可以捕获不同类型的异常
  2. 异常与对应类型的catch相匹配
  3. catch(...)可用于捕获任意类型的异常 ,防止未知的异常导致程序出错终止

三、异常的使用

1.异常的栈展开匹配

#include<iostream>
using namespace std;
double Division(int len, int times)
{
	if (times == 0)
	{
		throw"除零错误";
	}
	else
	{
		return (double)len / times;
	}
}
void func()
{
	throw 1;
}
void Func()
{
	try
	{
		int len, times;
		cin >> len >> times;
		cout << Division(len, times) << endl;
		func();
	}
	catch (const char* str)
	{
		cout << str << endl;
	}

}
int main()
{
	try
	{
		Func();
	}
	catch (const char* str)
	{
		cout << str << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

这里顺序是这样的,

  1. 先检查throw是否在try语句块中,如果在,则匹配相应的catch语句。
  2. 如果匹配不到或者不在try语句中,则推出当前函数栈,继续在调用函数的栈中进行查找匹配的catch语句。
  3. 如果到达main函数的栈,还是没有找到与之匹配的catch则会终止程序
  4. 如果匹配得到相应的catch语句并处理后,会继续执行catch后面的语句

值得注意的是:

在异常被抛出时,会创建一个异常对象的拷贝或移动,这个拷贝或移动的结果,是用于调用栈里向上传递的,直到找到于之匹配的catch语句块,这是为了保证抛出后,原始对象的状态的改变不会影响已经抛出的异常

在C++中抛出异常时,会创建一个异常对象的拷贝或者移动,以此来确保异常对象在抛出点和捕获点之间的生命周期和安全性。在C++11以后的版本中,如果异常类型支持移动语义并且操作是高效的,那么编译器偏向于使用移动构造来抛出异常。

2.异常的重新抛出

有时候我们可能发现在catch后不立即处理异常,而是选择重新抛出(rethrow)它,以便于让上层代码有机会处理它

在C++中,异常处理通常通过try-catch块实现。某些情况下,当前代码可能需要记录或部分处理异常,但仍希望上层调用者能够捕获并进一步处理。此时可以通过重新抛出(rethrow)实现。

try {
    // 可能抛出异常的代码
} catch (const std::exception& e) {
    // 记录异常信息
    std::cerr << "Log: " << e.what() << std::endl;
    // 重新抛出
    throw;
}

实际应用场景:

1.资源清理后重新抛出:

void processFile() {
    std::ifstream file("data.txt");
    try {
        if (!file) throw std::runtime_error("File open failed");
        // 处理文件内容
    } catch (...) {
        file.close(); // 确保资源释放
        throw; // 重新抛出异常
    }
}

在这个场景里可能因为抛异常导致不能让资源释放,所以现在catch语句里先释放空间,再将异常重新抛出。

异常重新抛出时,不会创建一个新的异常对象的拷贝或移动,而是继续传播当前捕获的异常对象

这个过程是高效的,因为它避免了不必要的对象的拷贝或移动,然而,这个也意味着在catch块中修改捕获的异常对象的状态可能会影响到上层代码对于这个对象的处理,因为它们是同一个对象,因此在重新抛出异常前,通常不建议修改捕获的异常对象的状态。

注意事项

  • 重新抛出的异常会保留原始异常类型和调用栈信息。
  • throw;只能在catch块中使用,否则会导致std::terminate
  • 若需要修改异常类型(如场景2),需显式抛出新异常而非使用throw;

2.异常安全

  • 构造函数完成对象的构造和初始化,最好不要在构造函数中抛异常,否则可能导致对象不完整或者没有完全初始化。
  • 析构函数主要完成资源的清理,最好不要在析构函数内部抛出异常,否则可能导致资源泄露(内存泄漏,句柄未关闭等等)

C++中异常会频繁导致资源泄漏的问题,比如在new和delete中间抛出来了异常,导致内存泄漏,在lock和unlock之间抛出异常导致死锁,C++通常使用RAII  来解决上面的问题(智能指针章节会讲到)

3.异常规范

//这个表示这个函数会抛出A/B/C/D中的某种类型
void func() throw(A,B,C,D);
//这个表示这个函数指挥抛出bad_allow的异常
void* operator new(std::size_t size) throw(std::bad_allow)
//这个表示不会抛出异常
void&operator delete (std::size_t size,void*ptr) throw();

//C++11中新增的noexcept表示不后悔抛出异常
thread() noexcept;
thread(thread&& x) noexcept; 
  • 函数的后面接throw(类型),列出这个函数可能抛出的所有异常类型
  • 函数后面接throw(),表示函数不抛异常
  • C++11新增关键字noexcept表示函数不抛异常

四、自定义异常体系

实际上,异常的抛出和捕获,在某种情况下不需要类型一一对应,那就是抛出派生对象,用基类来捕获(在实际中非常好用的做法)

#include <iostream>
#include <exception>
#include <string>

// 基类异常
class BaseException : public std::exception {
public:
    virtual const char* what() const noexcept override {
        return "BaseException occurred";
    }
};

// 派生类异常1
class DerivedExceptionA : public BaseException {
public:
    const char* what() const noexcept override {
        return "DerivedExceptionA occurred";
    }
};

// 派生类异常2
class DerivedExceptionB : public BaseException {
public:
    const char* what() const noexcept override {
        return "DerivedExceptionB occurred";
    }
};

void riskyFunction(int type) {
    if (type == 1) {
        throw DerivedExceptionA();
    }
    else if (type == 2) {
        throw DerivedExceptionB();
    }
}

int main() {
    try {
        riskyFunction(1);  // 测试派生类A
        // riskyFunction(2);  // 测试派生类B
    }
    catch (const BaseException& e) {  // 基类捕获所有派生类异常
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
    return 0;
}

用基类引用统一捕获不同的派生类对象,实现多态,规范了异常体系,防止乱抛异常导致项目程序终止。

五、标准库异常

C++标准库定义了一套异常类体系,均派生自std::exception基类,用于处理程序运行时的错误。这些异常类覆盖了常见错误场景,如内存分配失败、逻辑错误、范围越界等。

  1. std::exception
    所有标准库异常的基类,提供虚成员函数what()返回错误描述。

    try { /* 可能抛出异常的代码 */ } 
    catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
  2. std::runtime_error
    表示程序运行时才能检测的错误(如文件打开失败)。
    派生类包括:

    • std::overflow_error(算术溢出)
    • std::underflow_error(算术下溢)
    • std::system_error(系统调用错误)
  3. std::logic_error
    表示程序逻辑错误(如无效参数)。
    派生类包括:

    • std::invalid_argument(无效参数)
    • std::domain_error(数学定义域错误)
    • std::length_error(超出容器长度限制)
    • std::out_of_range(下标越界)

    ps:operator[]不会做边界检查,如果你尝试访问一个越界的索引,operator[]会返回对应位置的引用,但是这是一个未定义的行为,可能会导致程序崩溃或其他不可预测的结果。

  4. std::bad_alloc
    内存分配失败时由new抛出。

    try { int* arr = new int[1000000000000]; } 
    catch (const std::bad_alloc& e) {
        std::cerr << "Memory allocation failed: " << e.what() << std::endl;
    }
  5. std::bad_cast
    dynamic_cast对引用类型转换失败时抛出。

PS:C++标准异常库设计的不够好用,实际上很多公司都是自定义一套异常体系


自定义异常类

可通过继承std::exception或其派生类实现自定义异常:

class MyException : public std::runtime_error {
public:
    MyException(const std::string& msg) : std::runtime_error(msg) {}
};

try { throw MyException("Custom error"); } 
catch (const MyException& e) {
    std::cerr << e.what() << std::endl;
}


异常安全实践

  • RAII原则:通过智能指针(如std::unique_ptr)管理资源,避免内存泄漏。
  • noexcept规范:对不抛出异常的函数标记noexcept以优化性能。
  • 避免异常滥用:仅对异常情况使用异常(如关键错误),而非控制流程。

异常与性能

异常处理可能引入额外开销,但在现代编译器中,若无异常抛出,性能影响可忽略。建议在错误处理复杂或跨多层函数调用时使用异常。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐