C++基础系列【36】异常处理
博主介绍:程序喵大人
- 35- 资深C/C++/Rust/Android/iOS客户端开发
- 10年大厂工作经验
- 嵌入式/人工智能/自动驾驶/音视频/游戏开发入门级选手
- 《C++20高级编程》《C++23高级编程》等多本书籍著译者
- 更多原创精品文章,首发gzh,见文末
- 👇👇记得订阅专栏,以防走丢👇👇
😉C++基础系列专栏
😃C语言基础系列专栏
🤣C++大佬养成攻略专栏
🤓C++训练营
👉🏻个人网站
异常处理是一项重要的特性,它允许程序在运行时遇到错误条件时能够优雅地恢复或终止执行,而不是简单地崩溃。
C++通过try
、catch
和throw
三个关键字来实现异常处理机制。本文会详细介绍。
基本概念
定义
异常是指在程序执行过程中发生的、不符合程序正常流程的事件。在C++中,异常通常是由于某些错误条件触发的,如除以零、数组越界、内存分配失败等。
关键字
try
:用于标记可能会抛出异常的代码块,称为保护代码。throw
:当检测到异常条件时,使用throw
关键字抛出一个异常。throw
后面可以跟任意表达式,它的类型决定了抛出的异常类型。catch
:用于捕获并处理异常。catch
块紧跟在try
块之后,并指定了要捕获的异常类型。
用法
下面是一个简单的例子,演示了如何使用try
、throw
和catch
来处理除以零的异常:
#include <iostream>
using namespace std;
double division(int a, int b) {
if (b == 0) {
throw "Division by zero!"; // 抛出异常
}
return (a / b);
}
int main() {
int x = 50;
int y = 0;
double z = 0;
try {
z = division(x, y); // 可能抛出异常的代码
cout << z << endl;
} catch (const char* msg) { // 捕获并处理异常
cerr << msg << endl;
}
return 0;
}
在这个例子中,如果y
为零,division
函数将抛出一个字符串异常,该异常在main
函数的catch
块中被捕获并处理。
一个try
块可以跟随多个catch
块,来捕获多种不同类型的异常:
#include <iostream>
#include <stdexcept> // 包含标准异常类
using namespace std; // 为了演示方便,项目中不建议这样使用
void testFunction() {
throw runtime_error("Runtime error occurred!");
}
int main() {
try {
testFunction(); // 可能抛出异常的函数
} catch (const logic_error& e) {
cout << "Caught a logic_error: " << e.what() << endl;
} catch (const runtime_error& e) {
cout << "Caught a runtime_error: " << e.what() << endl;
} catch (...) { // 捕获所有其他类型的异常
cout << "Caught an unknown exception" << endl;
}
return 0;
}
testFunction
抛出了一个runtime_error
异常,该异常在第二个catch
块中被捕获并处理。最后一个catch
块使用省略号...
来捕获所有其他类型的异常。
C++标准异常类
C++标准库提供了一系列预定义的异常类,这些类都继承自std::exception
基类。
使用标准异常类可以使代码更加清晰。
层次结构
如图
暂时无法在飞书文档外展示此内容
std::exception
:所有标准异常的基类。std::bad_alloc
:内存分配失败时抛出。std::bad_cast
:动态类型转换失败时抛出。std::bad_typeid
:使用typeid
运算符失败时抛出。std::bad_exception
:在函数声明中使用了异常规格,但抛出了未列出的异常时抛出(C++11已弃用)。std::logic_error
:逻辑错误异常基类,包括:std::domain_error
:数学域错误,如sqrt(-1)。std::invalid_argument
:无效参数错误。std::length_error
:超出允许长度的错误。std::out_of_range
:范围错误,如访问vector的非法索引。
std::runtime_error
:运行时错误异常基类,包括:std::overflow_error
:上溢错误。std::range_error
:范围错误(与std::out_of_range
不同,用于其他情况)。std::underflow_error
:下溢错误。
示例
#include <iostream>
#include <stdexcept>
using namespace std;
void testLogicError() {
throw invalid_argument("Invalid argument error!");
}
void testRuntimeError() {
throw out_of_range("Out of range error!");
}
int main() {
try {
testLogicError(); // 抛出逻辑错误异常
} catch (const logic_error& e) {
cout << "Caught a logic_error: " << e.what() << endl;
}
try {
testRuntimeError(); // 抛出运行时错误异常
} catch (const runtime_error& e) {
cout << "Caught a runtime_error: " << e.what() << endl;
}
return 0;
}
testLogicError
函数抛出了一个invalid_argument
异常,而testRuntimeError
函数抛出了一个out_of_range
异常。这两个异常分别在对应的catch
块中被捕获并处理。
自定义异常类
虽然C++标准库提供了丰富的异常类,但在某些情况下,开发者可能需要定义自己的异常类。
我们可以通过继承std::exception
基类并重载what
方法来实现。
示例
#include <iostream>
#include <exception>
#include <string>
using namespace std;
class MyException : public exception {
public:
MyException(const string& message) : message_(message) {}
virtual const char* what() const noexcept override {
return message_.c_str();
}
private:
string message_;
};
void testCustomException() {
throw MyException("Custom exception occurred!");
}
int main() {
try {
testCustomException(); // 抛出自定义异常
} catch (const MyException& e) {
cout << "Caught a MyException: " << e.what() << endl;
} catch (const exception& e) {
cout << "Caught an unknown exception: " << e.what() << endl;
}
return 0;
}
MyException
类继承自std::exception
并重载了what
方法。testCustomException
函数抛出了一个MyException
异常,该异常在main
函数的catch
块中被捕获并处理。
注意,这里还添加了一个捕获所有其他std::exception
子类的catch
块,确保能够捕获所有未知异常。
noexcept
从C++11开始,推荐使用noexcept
关键字来声明函数不抛出任何异常:
void func() noexcept; // 声明函数不抛出任何异常
如果func
函数在执行过程中抛出了异常,程序会直接终止。noexcept
关键字还可以用于提高性能,因为编译器可以优化不抛出异常的函数调用。
如果你看过gcc
源码,你会发现,基本上通篇都是noexcept
。
noexcept
会告诉编译器,它修饰的函数不会产生异常(exception
),这有利于编译器做更多的优化。
C++
的异常处理是在运行时检测的,而不是在编译时检测,为了运行时检测,编译器应该会做些额外的操作,如果能够通过noexcept
明确的告诉编译器这个函数不会抛出异常,编译器应该会做一些优化。
验证函数是否noexcept
?
noexcept
还可以当作运算符,它可以传入参数,来验证某个函数是否是noexcept
:
void may_throw();
void no_throw() noexcept;
int main() {
noexcept(may_throw()); // false
noexcept(no_throw()); // true
}
什么时候使用noexcept
?
- 移动构造函数,移动赋值函数,建议使用
noexcept
修饰,因为搭配标准库使用时,noexcept
作用巨大,它可以优先移动而非拷贝,推荐阅读 move_if_noexcept。 - 而析构函数默认就是
noexcept
的,不需要显式指定noexcept
。 - 在明确确认某个函数不会产生
exception
时,可以使用noexcept
。
推荐看看这几个noexcept相关的文档:
- https://www.cnblogs.com/sword03/p/10020344.html
- https://en.cppreference.com/w/cpp/language/noexcept_spec
- https://en.cppreference.com/w/cpp/language/noexcept
- https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Re-noexcept
- https://stackoverflow.com/questions/10787766/when-should-i-really-use-noexcept
- https://www.heise.de/blog/C-Core-Guidelines-Der-noexcept-Spezifier-und-Operator-4121657.html
异常安全性
异常安全性是指程序在遇到异常时仍然能够保持正确的状态,不会出现资源泄露或者数据不一致等问题。
因为C++允许程序在执行过程中抛出异常,这可能导致程序的控制流发生变化,如果资源管理不当,就可能出现资源泄露或程序崩溃等问题。
码字不易,欢迎大家点赞,关注,评论,谢谢!
C++训练营
专为校招、社招3年工作经验的同学打造的1V1 C++训练营,量身定制学习计划、每日代码review,简历优化,面试辅导,已帮助多名学员获得大厂offer!