C++面向对象整理(10)之异常与捕获(try、catch、throw、noexcept)
注:整理一些突然学到的C++知识,随时mark一下
例如:忘记的关键字用法,新关键字,新数据结构
C++ 的 异常的捕获
提示:本文为 C++ 中 异常、try、throw、catch 的写法和举例
一、异常与捕获
1、C++异常处理关键字:try, throw, catch
throw:用于在代码块中抛出异常。格式是throw 变量或对象名字
,可以是匿名对象即类名()
,可以出现异常的代码放到try块中,然后利用throw抛出异常。
try:用于包围可能出现异常的代码块。格式是try {}
。
catch:用于捕获并处理异常。格式是catch (类型) {}
,会根据捕获到的数据类型写多个catch语句。catch与try配合出现。
将可能抛出异常的代码放入try块中,这样当异常发生时,程序不会立即崩溃,而是会跳转到与之关联的catch块进行处理。一旦异常被捕获并处理完毕后,控制流将离开catch块,并继续执行try-catch结构之后的代码。
当检测到某个错误或异常情况时,可以使用throw关键字抛出一个异常。这可以是内置的数据类型(如int,char等),也可以是自定义的类类型。
示例:
throw "An error occurred"; // 抛出字符串字面量
throw 0; // 抛出整数值
throw MyException(); // 抛出自定义异常对象
//利用catch捕获异常
catch块用于捕获并处理异常。你可以根据抛出的异常类型来编写不同的catch块。
示例:
try {
// ... 可能会抛出异常的代码,里面会有throw语句 ...
} catch (const char* msg) {
// 处理字符串类型的异常
} catch (int num) {
// 处理整数类型的异常
} catch (const MyException& e) {
// 处理自定义异常
}
//catch(类型) 如果想捕获其他类型再用
catch(...){
//处理其他的
}
当你不确定可能会抛出什么类型的异常,或者想要捕获所有类型的异常时,可以只使用catch(…)。这是一个捕获所有异常的通用catch块。
在catch块中,如果你不想处理异常,而是想将其继续向上抛出,可以在catch块中再次使用throw关键字(没有参数),这将重新抛出最近捕获的异常。
示例:
catch (const std::exception& e) {
std::cerr << "Caught an exception: " << e.what() << std::endl;
throw; // 重新抛出异常
}
异常必须有代码进行处理,不去处理话程序自动调用terminate函数触发中断。如果异常没有被任何catch块捕获,程序将调用std::terminate函数,这通常会导致程序终止。
2、try、catch、throw配合使用的例子
以下是一个不包含自定义异常类的简单例子,仅使用C++标准库中的异常类型,并展示了try、catch和throw的用法:
#include <iostream>
#include <stdexcept> // 包含标准异常类
// 一个函数,当条件不满足时抛出异常
void checkCondition(bool condition) {
if (!condition) {
throw std::runtime_error("Condition is not met!"); // 抛出运行时异常
}
std::cout << "Condition is met, no exception thrown." << std::endl;
}
int main() {
try {
// 调用函数并传入一个不满足条件的值
checkCondition(false);
// 如果checkCondition没有抛出异常,这里的代码将会执行
std::cout << "Continuing after checkCondition..." << std::endl;
} catch (const std::runtime_error& e) {
// 捕获运行时异常并处理
std::cerr << "Caught an exception: " << e.what() << std::endl;
// 在这里可以执行一些错误处理逻辑
} catch (...) {
// 捕获所有其他类型的异常
std::cerr << "Caught an unknown exception!" << std::endl;
// 这里处理未知异常的通用逻辑
}
// 无论是否抛出异常,都会执行到这里
std::cout << "Program continues after exception handling (if any)." << std::endl;
return 0;
}
在这个例子中:
checkCondition函数检查一个条件(在这里是一个布尔值)。如果条件不满足(即condition为false),函数会抛出一个std::runtime_error异常。在main函数中,我们尝试调用checkCondition(false),这会导致异常被抛出。
第一个catch块捕获std::runtime_error类型的异常,并打印出异常信息。第二个catch块是一个通用捕获块,它可以捕获所有其他类型的异常(虽然在这个简单的例子中可能不会被用到)。
如果checkCondition函数中的条件被满足(即传入true),则不会抛出异常,程序将输出“Condition is met, no exception thrown.”并继续执行后续代码。
3、自定义数据类型的异常抛出
异常可以是自定义数据类型,可以创建自己的类来表示特定的异常,并在throw语句中抛出这些类的实例。这
一般使用标准库中的exception类,继承他并重写what函数,示例:
class MyException : public std::exception {
public:
const char* what() const noexcept override {
return "My custom exception occurred";
}
};
throw MyException(); // 抛出自定义异常
如果想要创建自定义的异常类并重写what函数,需要从标准异常类(如std::exception)继承,并覆盖what成员函数。what
函数是一个返回const char*
类型的成员函数,它应该返回描述异常原因的字符串。
下面是一个简单的例子,展示了如何创建一个自定义异常类并重写what函数:
#include <iostream>
#include <string>
#include <exception> // 包含std::exception
// 自定义异常类
class MyException : public std::exception {
private:
std::string message; // 用于存储异常信息的字符串
public:
// 构造函数,接受一个字符串作为异常信息
explicit MyException(const std::string& msg) : message(msg) {}
// 重写what函数,返回异常信息
const char* what() const noexcept override {
return message.c_str();
}
};
int main() {
try {
// 抛出自定义异常
throw MyException("This is a custom exception!");
} catch (const MyException& e) {
// 捕获自定义异常并处理
std::cerr << "Caught an exception: " << e.what() << std::endl;
} catch (const std::exception& e) {
// 捕获所有标准异常
std::cerr << "Caught a standard exception: " << e.what() << std::endl;
} catch (...) {
// 捕获所有其他类型的异常
std::cerr << "Caught an unknown exception!" << std::endl;
}
return 0;
}
在这个例子中:
MyException类从std::exception继承。
它有一个私有成员变量message,用于存储异常信息。
构造函数接受一个字符串参数,并将其赋值给message。
what函数被重写以返回message的C风格字符串表示(通过调用std::string::c_str())。注意,what函数的返回类型是const char*,因此我们不能直接返回std::string对象。相反,我们返回std::string对象内部字符数组的指针,这可以通过调用c_str()成员函数来实现。
在main函数中,我们抛出一个MyException实例,并使用catch块捕获它。捕获到异常后,我们调用e.what()来获取并打印异常信息。
此外,what函数的声明中包含了noexcept关键字,这表示该函数不会抛出任何异常。这是因为在异常处理过程中,如果what函数本身又抛出了异常,那么程序的行为将是未定义的。因此,当你重写what函数时,也应该确保其是noexcept的。
4、值捕获、引用捕获、指针捕获
我们更倾向于使用值或引用捕获异常,因为它们更简单、更安全,并且避免了手动内存管理的需要。只有在特定情况下,例如当异常对象非常大且不可能被复制时,才应考虑使用动态分配和指针捕获。但即使在这种情况下,也应确保正确地管理内存。
推荐的做法是直接抛出异常对象,并使用引用捕获来避免不必要的复制。避免抛出指针或动态分配的对象,除非有明确的理由和适当的内存管理策略。
throw MyException();
catch (MyException &e){ //...}
使用引用捕获(catch (MyException &e)
)是推荐的做法,因为它避免了不必要的复制。异常对象直接传递给catch块中的引用,不会调用拷贝构造函数。因此,它更高效,特别是当异常对象较大或包含资源时。
5、noexcept
在C++中,如果你想限定抛出异常的类型,你可以使用异常规格(exception specifications)。然而,如果你需要指定函数可能抛出的异常类型,可以使用异常规格列表。从C++17开始,这种旧式的异常规格列表已经被废弃,推荐使用noexcept
关键字来表明一个函数不会抛出任何异常。
在C++17之前的版本中,你可以这样声明一个函数的异常规格列表来指定类型:
void myFunction() throw(MyException); // 指定只能抛出MyException类型的异常
上面的代码表示myFunction函数只能抛出MyException类型的异常。如果函数试图抛出其他类型的异常,编译器会报错。然而,这种异常规格方式在C++11及以后的版本中被认为是不够灵活的,并且在C++17中已被弃用。
推荐使用noexcept关键字
来指定函数不会抛出异常:
void myFunction() noexcept; // 声明函数不会抛出任何异常
如果函数确实可能抛出异常,并且你希望不限制异常类型,则不需要使用任何异常规格。如果你确实需要声明可能抛出的异常类型,建议使用文档或注释来说明这一点,而不是使用已弃用的异常规格语法。
如果你确实想要通过接口(即基类)来限定派生类能抛出的异常类型,你可以使用纯虚函数和异常规格结合的方式,但这种方式比较复杂,且由于旧式异常规格已被弃用,不推荐这样做。