CPP异常
异常
一、异常介绍
异常是程序运行时出现的预期之外的情况或错误,导致程序运行终止。
例如下面代码:
int main()
{int num1 = 100;int num2 = 0;int num3 = num1 / num2; cout << "运算结果:" << num3 << endl;return 0;
}
除数为0,程序出现异常,导致程序运行终止。
在比如,下面代码,虽然通过stream.fail()
检查到了错误,并输出了错误信息,但这并不足以处理错误;特别是对调用者来说无法进行进一步处理。
void readFile()
{fstream stream;stream.open("example.txt");if (stream.fail()){cout << "加载文件失败" << endl;return;}
}
C++ 提供了异常处理机制,用于在程序执行过程中处理非预期的、可能导致程序无法继续执行的情况,从而避免程序崩溃,并提供恢复或处理的机会。
相比于仅仅打印错误,C++ 的异常机制有以下优势:
- 错误处理和业务逻辑分离
-
如果你只是
cout << "错误信息"
,那么错误处理逻辑和正常业务逻辑就混在一起,代码很乱。 -
用异常机制,
try
块只管写业务逻辑,错误处理交给catch
块,职责清晰。✅ 例子:
int divide(int a, int b) {if (b == 0)throw std::runtime_error("除数不能为 0");return a / b; }int main() {try {cout << divide(10, 0) << endl; // 正常业务逻辑}catch (const std::runtime_error& e) {cout << "异常捕获: " << e.what() << endl; // 错误处理}cout << "程序继续运行" << endl; }
2. 错误能“传递”给调用者
- 打印错误信息只能停留在本函数里,调用者完全不知道出错了。
- 异常机制可以把错误“抛”给调用者,由调用者决定怎么处理,更灵活。
✅ 例子:
void readFile(const string& filename)
{fstream file(filename);if (!file.is_open())throw std::runtime_error("文件打开失败: " + filename);// ...
}int main()
{try {readFile("not_exist.txt"); // 子函数出错}catch (const std::runtime_error& e) {cout << "在 main 中捕获异常: " << e.what() << endl; // 调用者决定处理方式}
}
如果只是 cout << "文件打开失败"
,那调用者完全无法知道出错情况。
- 可以分类处理不同类型错误
- 异常可以携带不同的类型(int、string、std::exception…),调用者可以根据类型区分处理方式。
cout
只能一股脑打印出来。
✅ 例子:
try {throw 404; // 整型异常// throw "文件丢失"; // 字符串异常// throw std::runtime_error("磁盘错误"); // 标准异常
}
catch (int code) {cout << "错误码: " << code << endl;
}
catch (const char* msg) {cout << "错误信息: " << msg << endl;
}
catch (const std::exception& e) {cout << "标准异常: " << e.what() << endl;
}
- 异常可以中断当前流程,直接跳转到 catch
- 一旦
throw
,会立刻跳出当前的执行流程,转到catch
,省去了层层if-else
判断。 - 打印错误信息并不会阻止后续代码继续执行,可能会造成逻辑混乱。
C++ 的异常处理主要基于三个关键字:try、catch、throw
try
块:包含可能出现问题的代码。如果在try
块中的代码抛出异常,那么控制流将立即离开try
块,并寻找与该异常匹配的catch
块。throw
语句:当问题出现时,通过throw
语句抛出一个异常。这会导致程序的控制流立即离开当前的try
块,并寻找匹配的catch
块。catch
块:catch
块用于捕获并处理异常。每个catch
块都指定它可以处理的特定类型的异常。如果try
块中抛出的异常与某个catch
块的类型匹配,那么该catch
块的代码就会被执行。
下面通过一个简单的示例,理解try / catch块处理异常的执行流程:
int main()
{try {int a = 10;int b = 0;if (b == 0){throw "除数不能为0";}int result = a / b;}catch (const char* error){cout << error << endl;}cout << "程序执行结束" << endl;return 0;
}
在上述案例中执行结果中,我们看到输出了除数不能为0的信息提示,处理了程序因意外情况所导致的终止问题。
在try
块中执行可能会产生异常的代码,通过throw
关键字抛出了一个字符串类型的异常。程序抛出异常后,throw
后面的代码会跳过,不会在继续执行,转而跳转到对应匹配的catch
块中执行。catch
块中处理完异常后继续执行catch
块后面的代码。
控制流示意图
程序开始|vtry {语句1; <-- 正常执行语句2; <-- 正常执行throw 异常; <-- 一旦执行,立刻跳出 try语句3; <-- (不会执行!!!)}catch(异常类型 e) {处理异常的代码;}|v程序继续执行 (catch 后面的部分)
throw
关键字后可以是任何类型的值,我们也可以将整型值作为异常抛出。例如:
int main()
{try {int a = 10;int b = 0;if (b == 0){throw -1;}int result = a / b;}catch (const char* error){cout << error << endl;}catch (const int error){cout << "产生异常" << endl; }cout << "程序执行结束" << endl;return 0;
}
在这里因为抛出的是整型异常,因此异常类型与catch(const int error)
块中捕获的异常类型相同,进而会执行该catch
块中的代码。
但是随便抛出这种异常很不规范,因此C++中引出了标准异常。
throw
在 try 内部。- 一旦
throw
执行,try
里面 后续语句直接被跳过。 - 程序会转到匹配的
catch
执行。 - 如果没有匹配的
catch
,程序会调用terminate()
直接结束。terminate()
是 C++ 标准库里的一个函数(定义在<exception>
头文件里)。- 👉 当程序抛出了异常,但没有找到任何合适的
catch
来处理时,C++ 就会调用std::terminate()
,强行结束程序。
二、标准异常
C++标准库中定义了各种各样预置的异常类,其中Exception类是所有标准异常的基类,异常类都派生自此类。因此,通过捕获此类型可以捕获所有标准异常 。
Exception类定义在<exception>
头文件中,而它的子类大多数定义在<stdexcept>
中,下面列出一些从Exception类派生的类。
- 从Exception直接派生的异常类
异常名 | 说明 |
---|---|
logic_error | 逻辑类异常的基类 |
runtime_error | 运行时异常的基础 |
bad_alloc | 动态分配内存失败的异常 |
bad_typeid | 使用typeid在NULL指针产生的异常 |
bad_cast | dynamic_cast转换失败异常 |
ios_base::failure | I/O输入输出异常 |
- 从logic_error派生的逻辑异常
异常名 | 说明 |
---|---|
length_error | 超出容器最大长度异常 |
out_of_range | 越界异常 |
invalid_argument | 参数不合适异常 |
下面是一个手动抛出标准异常的示例:
int main()
{try{std::ifstream stream("helloworld.txt");if (!stream){throw std::ios_base::failure("打开文件失败");}}catch (const std::ios_base::failure& e){cout << e.what() << endl;}cout << "程序运行结束" << endl;return 0;
}
使用标准异常可以使异常信息更加明确,因为开发人员一看异常类型就知道大致发生了什么类型的错误,无需猜测异常的含义。
通过捕获特定的预置异常类型,可以针对性地处理不同类型的错误,避免了对所有异常进行统一处理可能导致的过度简化或错误处理不当。例如,当捕获到std::bad_alloc
时,可以尝试释放一些临时资源或通知用户内存不足;捕获到std::ios_base::failure时,可以检查磁盘文件或权限问题。
捕获不同的标准异常,可以写不同的处理逻辑,而不是“一锅端”。
例如:
try {// 可能抛出异常的代码
}
catch (const std::bad_alloc& e) {cerr << "内存不足: " << e.what() << endl;// 释放资源 / 通知用户
}
catch (const std::ios_base::failure& e) {cerr << "文件IO错误: " << e.what() << endl;// 检查文件路径、权限
}
catch (const std::exception& e) {cerr << "其他标准异常: " << e.what() << endl;
}
这样不同错误都有“对症下药”的处理办法。
三、try/catch块
1、try/catch
异常处理主要通过try-catch
语句实现。try
块包围可能抛出异常的代码,而catch
块紧跟在try
块之后,用于捕获并处理特定类型的异常。一个try
块后面可以跟多个catch
块,用于捕获不同类型的异常。
#include <iostream>
using namespace std;
int main()
{try{int a = 10;int b = 0;if (b == 0){throw runtime_error("除数不能为0");}int c = a / b;}catch (const std::runtime_error& e){cout << e.what() << endl;}catch (const std::exception& e){cout << e.what() << endl;}return 0;
}
存在多个catch块时,捕获异常的顺序,先捕获子类异常,最后捕获父类异常。因为代码中抛出异常后,会与对应的catch块所捕获的异常进行匹配,如果匹配到其中的某个异常类型一致,则不会执行其他catch块语句。在这里如果先捕获exception
类型的异常,则其他catch块则不会有机会执行。
2、万能捕获
C++中可以使用下面语法处理任何类型的异常:
catch(...)
{
}
catch(const std::exception& e)
仅捕获那些继承自标准库基类 std::exception
或其派生类的异常。这类异常通常涵盖了标准库自身抛出的大部分异常。
而catch(...)
能够捕获所有类型的异常,无论异常是什么类型,甚至是用户自定义的异常类型。
示例:
int main()
{try{throw 1;}catch (const std::exception&){cout << "exception异常" << endl;}catch (...){cout << "程序异常" << endl;}cout << "程序运行结束" << endl;return 0;
}
在实际编程中,通常会根据需要同时使用这两种捕获方式,形成一个异常处理链。
try {// 可能抛出异常的代码throw -1;
} catch (const std::runtime_error& re) {// 特定类型异常处理std::cerr << "Caught runtime error: " << re.what() << std::endl;
} catch (const std::exception& e) {// 处理标准库中其他 std::exception 派生类的异常std::cerr << "Caught standard exception: " << e.what() << std::endl;
} catch (...) {// 作为兜底,捕获所有未被前面 catch 块处理的异常std::cerr << "Caught an unknown exception." << std::endl;
}
这样的结构确保了:
- 具体类型的异常能得到有针对性的处理。
- 标准库中常见的异常可以被识别并获取详细信息。
- 所有未被前面特定
catch
块处理的其他异常(包括自定义异常,只要它们不是std::exception
的派生类)都能被捕获,防止程序意外终止。
四、throw语句
1、抛出异常
throw
关键字用于中断正常的程序执行流程,将控制权转移到与之匹配的异常处理代码(即 catch
块)。当程序中某个条件不满足、发生错误或遇到无法继续执行的情况时,通过 throw
抛出一个异常对象,表示出现了异常状况。
throw exception_object;
抛出的异常对象可以是以下类型:
- 基本类型:如
int
、float
、bool
等。 - 复合类型:如指针、数组、字符串(如
std::string
)、结构体、类等。 - 标准库异常类:如
std::runtime_error
、std::logic_error
、std::bad_alloc
等,它们提供了统一的接口(如what()
方法)来获取错误信息。 - 自定义异常类:程序员可以定义自己的异常类,通常会继承自
std::exception
或其子类,添加特定的错误信息成员和辅助方法。
#include <iostream>
#include <stdexcept>
void divide(int numerator, int denominator) {if (denominator == 0) {throw std::invalid_argument("Denominator cannot be zero.");}std::cout << "Result: " << (numerator / denominator) << std::endl;
}
int main() {try {divide(10, 0); // 这里将触发异常} catch (const std::invalid_argument& e) {std::cerr << "Error: " << e.what() << std::endl;}return 0;
}
what()
是std::exception
类的一个成员函数。- 因为所有标准异常类(例如
runtime_error
,logic_error
,out_of_range
等)都继承自std::exception
,所以它们都有what()
方法。
2、声明异常
声明异常是指在函数接口中指定该函数可能抛出的异常类型。
语法
在函数声明或定义处,紧随函数之后,使用 throw
关键字和一对圆括号列出可能抛出的异常类型列表。如果函数不抛出任何异常,可以声明为 throw()
(在C++11及以后版本中,推荐使用 noexcept
替代)
// 声明可能抛出特定类型的异常
void function1() throw(std::exception, std::bad_alloc);
// 声明不抛出任何异常(C++11之前)
void function2() throw();
// 使用noexcept声明不抛出任何异常(C++11及以后)
void function3() noexcept;
C++11 之后推荐使用 noexcept
代替 throw()
来声明函数不会抛出异常。
优点:
- 编译器可以优化代码(例如移动操作可无异常保证)。
- 更清晰、更现代化的写法。
如果函数内部抛出异常,程序会调用 std::terminate()
,直接终止。
五、自定义异常
C++中的自定义异常是指程序员根据实际需求创建的、用来表示特定类型错误或异常情况的类。自定义异常通常是为了更好地适应特定的应用场景,提供比标准库异常类更具体的错误描述和额外的上下文信息。
下面详细说明如何定义、抛出和捕获自定义异常。
1、定义自定义异常类
自定义异常类通常遵循以下步骤:
(1)继承标准库异常类:
自定义异常类通常继承自 std::exception
或其派生类,如 std::runtime_error
、std::logic_error
等。这样可以获得标准异常类提供的通用接口,如 what()
方法,用于获取异常的描述信息。
class CustomException : public std::runtime_error {
public:explicit CustomException(const std::string& message): std::runtime_error(message) {}
};
上述代码定义了一个名为 CustomException
的自定义异常类,它继承自 std::runtime_error
并在其构造函数中传入一个描述异常原因的字符串。
explicit
关键字的作用是阻止编译器进行隐式类型转换。没有 explicit
的情况下,如果一个构造函数只接受一个参数,那么该构造函数可以被编译器用来执行从参数类型到类类型的隐式转换:
1️⃣ 单参数构造函数可以隐式转换
如果一个类有一个 只接受一个参数的构造函数,C++ 编译器会默认允许从该参数类型隐式生成类对象。
示例:
template<typename T>
class MyClass {
public:MyClass(T data) : data(data) {}T getData() const { return data; }
private:T data;
};int main() {MyClass<int> obj = 10; // ✅ 隐式地把 int 转换为 MyClass<int>
}
这里 10
被自动转换成了 MyClass<int>(10)
。编译器帮你做了“隐式构造”。
2️⃣ 使用 explicit
阻止隐式转换
如果加了 explicit
:
explicit MyClass(T data) : data(data) {}
MyClass<int> obj = 10;
❌ 会报错- 必须显式调用构造函数:
MyClass<int> obj(10); // ✅ 正确
原因:防止编译器在不知情的情况下创建临时对象,避免逻辑错误或性能问题。
3️⃣ 为什么可能出错
例子:
MyClass<int> obj1(5);
int num = 10;
obj1 = num; // 编译器会隐式调用 MyClass<int>(num)
- 如果构造函数是隐式的,编译器自动把
num
转成MyClass<int>
临时对象,再用赋值操作赋给obj1
。 - 逻辑上可能不是你想要的,甚至可能有性能损耗。
同理,函数参数:
void process(const MyClass<int>& obj) { cout << obj.getData() << endl; }
process(10); // 隐式构造 MyClass<int>(10)
- 看起来“方便”,但其实程序内部创建了临时对象,可能造成误用或者混乱。
✅ 总结
- explicit 的作用:阻止单参数构造函数被编译器用作隐式类型转换。
- 使用场景:当你希望类对象只能显式创建,不允许编译器偷偷帮你转换类型。
- 好处:
- 代码逻辑更清晰
- 避免意外临时对象生成
- 函数调用、赋值行为可控
(2)添加成员变量和方法
根据需要,可以在自定义异常类中添加额外的成员变量来存储特定的错误信息、错误代码、上下文数据等。还可以提供额外的成员方法来访问这些信息。
class CustomException : public std::runtime_error {
public:enum ErrorCode {InvalidInput,ResourceNotFound,// 其他错误代码...};CustomException(ErrorCode code, const std::string& message): std::runtime_error(message), code_(code) {}ErrorCode getErrorCode() const { return code_; }
private:ErrorCode code_;
};
在这个例子中,自定义异常类包含了枚举类型的错误代码以及相应的构造函数和访问方法。
2、抛出自定义异常
抛出自定义异常与抛出标准库异常类似,使用 throw
关键字配合自定义异常对象即可:
void someFunction() {if (/* 检测到错误条件 */) {throw CustomException(CustomException::ErrorCode::InvalidInput, "Invalid input data.");}// 正常执行代码...
}
3、捕获自定义异常
捕获自定义异常与捕获标准库异常一样,使用 catch
块并指定自定义异常类的类型:
try {someFunction();
} catch (const CustomException& e) {std::cerr << "Caught custom exception: " << e.what() << " (Code: " << static_cast<int>(e.getErrorCode()) << ")" << std::endl;
}
优点与注意事项
优点:
- 针对性强:自定义异常可以精确地描述特定应用程序中的错误情况,提供更丰富的错误信息和上下文。
- 易于扩展:可以根据需要随时添加新的异常类,以适应软件演化过程中的新需求。
- 代码清晰:自定义异常使得异常处理代码更具可读性和自解释性,有助于理解和维护。
注意事项:
- 避免过度定制:如果标准库异常类已经足够描述问题,不必强行创建自定义异常。过度细化的异常类型可能导致代码复杂度增加,不易管理。
- 遵循继承规则:自定义异常类应合理继承标准库异常类,利用现有异常体系结构,以保持一致性和互操作性。