17、【C++】异常
内置类型:如int,简单但信息有限。标准异常派生类,提供what()方法返回错误信息。自定义类型:继承,实现自定义错误信息。示例// 内置类型异常try {// const char*类型// 标准异常try {自定义异常应继承,并重写what()private:public:// 重写what(),返回错误信息try {connect();// 输出"Error 404: connection
17、【C++】异常
目录
- 一、异常的基本概念
- 二、异常的语法
- 三、异常类型与传播
- 四、栈展开(Stack Unwinding)
- 五、异常安全(Exception Safety)
- 六、标准异常库
- 七、自定义异常
- 八、异常与构造函数/析构函数
- 九、noexcept关键字
- 十、异常的优缺点
- 十一、异常处理最佳实践
- 十二、常见误区与注意事项
一、异常的基本概念
1.1 为什么需要异常(相比错误码)
在C++中,处理错误的传统方式是错误码(如返回-1表示失败),但异常机制具有以下优势:
- 不污染返回值:函数返回值可专注于业务逻辑,无需保留错误码。
- 集中处理:错误处理代码与正常逻辑分离,提高可读性。
- 强制处理:未处理的异常会导致程序终止,避免错误被忽略。
- 传播性:异常可沿调用栈向上传播,直至被捕获,适合深层调用。
错误码示例(传统方式):
int divide(int a, int b) {
if (b == 0) return -1; // 错误码
return a / b;
}
// 使用时需检查返回值
int result = divide(10, 0);
if (result == -1) {
// 处理错误
}
异常示例:
int divide(int a, int b) {
if (b == 0) throw std::runtime_error("division by zero");
return a / b;
}
// 使用时集中处理异常
try {
int result = divide(10, 0);
} catch (const std::exception& e) {
std::cout << "错误:" << e.what() << std::endl;
}
1.2 异常的工作流程
- 抛出异常:通过
throw关键字抛出异常对象。 - 查找处理:编译器沿调用栈向上查找匹配的
catch块。 - 栈展开:销毁从
throw点到catch点之间的所有自动对象。 - 处理异常:执行匹配的
catch块代码。 - 继续执行:异常处理完成后,从
catch块后继续执行。
1.3 异常的核心术语
- 异常对象:
throw表达式创建的对象,用于传递错误信息。 - try块:可能抛出异常的代码块,后跟一个或多个
catch块。 - catch块:捕获并处理特定类型异常的代码块。
- 栈展开:异常抛出时,自动销毁局部对象的过程。
- 未捕获异常:未被任何
catch块捕获的异常,导致std::terminate调用。
二、异常的语法
2.1 try块(监控异常)
try块用于包裹可能抛出异常的代码,其后必须跟一个或多个catch块:
try {
// 可能抛出异常的代码
risky_operation();
} catch (const std::exception& e) {
// 处理异常
}
2.2 throw表达式(抛出异常)
throw用于抛出异常对象,类型可以是内置类型或自定义类型:
// 抛出内置类型
throw 42; // 抛出int类型异常
// 抛出标准异常
throw std::out_of_range("index out of bounds");
// 抛出自定义异常
throw MyException("custom error");
2.3 catch块(捕获异常)
catch块根据异常类型捕获异常,语法为catch (类型名 变量名):
try {
throw std::runtime_error("error");
} catch (const std::runtime_error& e) { // 捕获runtime_error
std::cout << e.what() << std::endl;
} catch (const std::exception& e) { // 捕获其他exception派生类
std::cout << e.what() << std::endl;
} catch (...) { // 捕获所有类型异常(兜底)
std::cout << "unknown error" << std::endl;
}
2.4 多个catch块的匹配顺序
catch块按声明顺序匹配异常类型,派生类异常必须在基类异常前捕获,否则派生类异常会被基类catch块捕获:
try {
throw std::runtime_error("runtime error");
} catch (const std::exception& e) { // 基类异常(错误:应放在派生类后)
std::cout << "exception: " << e.what() << std::endl;
} catch (const std::runtime_error& e) { // 派生类异常(永远不会被匹配)
std::cout << "runtime error: " << e.what() << std::endl;
}
正确顺序:
try {
throw std::runtime_error("runtime error");
} catch (const std::runtime_error& e) { // 派生类在前
std::cout << "runtime error: " << e.what() << std::endl;
} catch (const std::exception& e) { // 基类在后
std::cout << "exception: " << e.what() << std::endl;
}
三、异常类型与传播
3.1 异常类型(内置类型与自定义类型)
- 内置类型:如
int、const char*,简单但信息有限。 - 标准异常:
std::exception派生类,提供what()方法返回错误信息。 - 自定义类型:继承
std::exception,实现自定义错误信息。
示例:
// 内置类型异常
try {
throw "error message"; // const char*类型
} catch (const char* msg) {
std::cout << msg << std::endl;
}
// 标准异常
try {
throw std::logic_error("logical error");
} catch (const std::logic_error& e) {
std::cout << e.what() << std::endl;
}
3.2 异常传播(调用栈展开)
异常抛出后,若当前函数没有匹配的catch块,异常会沿调用栈向上传播,直到被捕获或程序终止:
void func3() {
throw std::runtime_error("error from func3"); // 抛出异常
}
void func2() {
func3(); // 未捕获异常,继续传播
}
void func1() {
func2(); // 未捕获异常,继续传播
}
int main() {
try {
func1(); // 异常传播到main函数
} catch (const std::exception& e) {
std::cout << "捕获异常:" << e.what() << std::endl;
}
return 0;
}
3.3 未捕获异常(std::terminate)
若异常传播至main函数仍未被捕获,系统调用std::terminate终止程序(默认调用std::abort):
void func() {
throw "未捕获的异常";
}
int main() {
func(); // 异常未被捕获,调用std::terminate
return 0;
}
3.4 异常对象的生命周期
异常对象由throw表达式创建,生命周期持续到最后一个捕获它的catch块结束:
std::string func() {
try {
throw std::string("exception object");
} catch (std::string& s) {
s = "modified"; // 修改异常对象
throw; // 重新抛出异常(保持原始类型)
}
}
int main() {
try {
func();
} catch (std::string& s) {
std::cout << s << std::endl; // 输出"modified"
}
return 0;
}
四、栈展开(Stack Unwinding)
4.1 栈展开的过程
当异常抛出时,编译器自动执行以下操作:
- 销毁当前函数中从
throw点到函数入口之间的所有自动对象(按构造逆序)。 - 返回到调用函数,重复步骤1,直到找到匹配的
catch块。 - 若找到
catch块,执行该块代码;否则继续传播。
4.2 局部对象的析构
栈展开过程中,所有自动存储期对象的析构函数会被调用,确保资源释放:
class MyObject {
public:
~MyObject() { std::cout << "MyObject destroyed" << std::endl; }
};
void func() {
MyObject obj; // 自动对象
throw std::runtime_error("error"); // 抛出异常,obj的析构函数被调用
}
int main() {
try {
func();
} catch (...) {}
// 输出:MyObject destroyed
return 0;
}
4.3 栈展开的示例(代码+图解)
代码:
void func3() {
MyObject obj3;
throw std::runtime_error("from func3");
}
void func2() {
MyObject obj2;
func3();
}
void func1() {
MyObject obj1;
func2();
}
int main() {
try {
func1();
} catch (const std::exception& e) {
std::cout << e.what() << std::endl;
}
return 0;
}
栈展开顺序:
func3抛出异常,销毁obj3→ 调用obj3.~MyObject()。- 返回
func2,销毁obj2→ 调用obj2.~MyObject()。 - 返回
func1,销毁obj1→ 调用obj1.~MyObject()。 - 返回
main,catch块捕获异常。
输出:
MyObject destroyed (obj3)
MyObject destroyed (obj2)
MyObject destroyed (obj1)
from func3
五、异常安全(Exception Safety)
5.1 异常安全的三个级别
5.1.1 基本保证(Basic Guarantee)
异常抛出后:
- 程序状态保持一致(不崩溃、资源不泄漏)。
- 对象 invariants(不变式)被维护。
- 但具体状态可能不确定。
示例:
void basic_guarantee(std::vector<int>& v, int index, int value) {
if (index >= v.size()) {
throw std::out_of_range("index");
}
v[index] = value; // 若抛出异常(如value的构造函数),v可能被部分修改
}
5.1.2 强保证(Strong Guarantee)
异常抛出后:
- 程序状态完全回滚到异常抛出前的状态(仿佛操作从未执行)。
- 要么操作成功,要么没有任何副作用。
示例(使用copy-and-swap):
void strong_guarantee(std::vector<int>& v, int index, int value) {
std::vector<int> temp(v); // 拷贝副本
temp[index] = value; // 在副本上操作
v.swap(temp); // 交换,无异常风险
}
5.1.3 不抛出保证(No-Throw Guarantee)
函数绝不抛出异常,无论输入或环境如何。
示例:
void no_throw_guarantee(int& a, int& b) noexcept {
std::swap(a, b); // std::swap是noexcept的
}
5.2 RAII与异常安全
RAII(资源获取即初始化) 是实现异常安全的核心技术,通过对象生命周期管理资源:
// RAII封装文件资源
class File {
public:
File(const char* filename) : _fd(fopen(filename, "r")) {
if (!_fd) throw std::runtime_error("open failed");
}
~File() { if (_fd) fclose(_fd); } // 确保关闭
private:
FILE* _fd;
};
void func() {
File file("data.txt"); // RAII对象,异常抛出时自动关闭文件
// ... 可能抛出异常的操作 ...
}
5.3 编写异常安全的代码
- 优先使用RAII:如
std::unique_ptr、std::lock_guard。 - 使用copy-and-swap:实现强异常安全。
- 避免在构造函数中分配多个资源:可使用工厂函数。
- 析构函数不抛出异常:确保栈展开时资源释放。
六、标准异常库
6.1 std::exception基类
C++标准库异常均继承自std::exception,该类提供what()方法返回错误描述:
class exception {
public:
virtual const char* what() const noexcept; // 返回错误信息
virtual ~exception() noexcept;
};
6.2 常见派生异常类
6.2.1 逻辑错误(std::logic_error)
表示程序逻辑错误(编译期可检测):
std::domain_error:参数域错误。std::invalid_argument:无效参数。std::length_error:长度超出范围。std::out_of_range:索引越界(如vector::at)。
6.2.2 运行时错误(std::runtime_error)
表示运行时错误(编译期不可检测):
std::range_error:数值超出有效范围。std::overflow_error:算术上溢。std::underflow_error:算术下溢。std::system_error:系统调用错误(如IO错误)。
6.3 标准异常的使用示例
#include <vector>
#include <stdexcept>
int main() {
std::vector<int> v(5);
try {
v.at(10) = 42; // 抛出std::out_of_range
} catch (const std::out_of_range& e) {
std::cout << "out_of_range: " << e.what() << std::endl;
}
try {
throw std::invalid_argument("invalid argument");
} catch (const std::invalid_argument& e) {
std::cout << "invalid_argument: " << e.what() << std::endl;
}
return 0;
}
七、自定义异常
7.1 继承std::exception
自定义异常应继承std::exception,并重写what()方法:
#include <exception>
#include <string>
class MyException : public std::exception {
private:
std::string _msg;
public:
MyException(const std::string& msg) : _msg(msg) {}
// 重写what(),返回错误信息
const char* what() const noexcept override {
return _msg.c_str();
}
};
7.2 重写what()方法
what()方法应返回C风格字符串,且不抛出异常(使用noexcept):
class NetworkException : public std::exception {
private:
std::string _msg;
public:
NetworkException(int code, const std::string& msg) {
_msg = "Error " + std::to_string(code) + ": " + msg;
}
const char* what() const noexcept override {
return _msg.c_str();
}
};
7.3 自定义异常的抛出与捕获
void connect() {
throw NetworkException(404, "connection failed");
}
int main() {
try {
connect();
} catch (const NetworkException& e) {
std::cout << e.what() << std::endl; // 输出"Error 404: connection failed"
} catch (const std::exception& e) {
// 处理其他异常
}
return 0;
}
八、异常与构造函数/析构函数
8.1 构造函数抛出异常
构造函数抛出异常表示对象未完全构造,其析构函数不会被调用:
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructed" << std::endl;
throw std::runtime_error("construction failed");
}
~MyClass() { std::cout << "MyClass destroyed" << std::endl; }
};
int main() {
try {
MyClass obj; // 构造函数抛出异常
} catch (...) {}
// 输出:MyClass constructed(析构函数未被调用)
return 0;
}
8.2 析构函数不应抛出异常
析构函数抛出异常会导致程序终止(栈展开过程中遇到第二个异常):
class BadClass {
public:
~BadClass() noexcept(false) { // 析构函数抛出异常(危险)
throw "destructor exception";
}
};
int main() {
try {
BadClass obj;
throw "main exception"; // 第一个异常
} catch (...) {}
// 析构函数抛出第二个异常,调用std::terminate
return 0;
}
8.3 示例:构造函数异常导致资源泄漏
若构造函数中分配多个资源,部分资源可能因异常未释放:
class Resource {
public:
Resource(int id) : _id(id) {
std::cout << "Resource " << _id << " acquired" << std::endl;
}
~Resource() {
std::cout << "Resource " << _id << " released" << std::endl;
}
private:
int _id;
};
class ResourceUser {
public:
ResourceUser() : _res1(1) {
// _res1构造成功,_res2构造前抛出异常
throw std::runtime_error("error");
Resource _res2(2); // 未执行
}
private:
Resource _res1; // 构造成功
Resource _res2; // 未构造
};
int main() {
try {
ResourceUser user;
} catch (...) {}
// 输出:Resource 1 acquired(_res1未释放,内存泄漏)
return 0;
}
解决:使用RAII和成员初始化列表顺序:
class ResourceUser {
public:
ResourceUser() : _res2(2), _res1(1) {} // 按声明顺序初始化,而非列表顺序
private:
Resource _res1; // 先声明,后初始化
Resource _res2; // 后声明,先初始化
};
九、noexcept关键字
9.1 noexcept的语法与作用
noexcept用于指定函数是否可能抛出异常:
// 函数不抛出异常
void safe_func() noexcept {
// ... 不抛出异常的代码 ...
}
// 函数可能抛出异常(默认)
void risky_func() noexcept(false) {
throw std::runtime_error("risky");
}
9.2 noexcept操作符
noexcept(表达式)根据表达式是否可能抛出异常返回true或false:
void func() noexcept;
void func2();
constexpr bool b1 = noexcept(func()); // true
constexpr bool b2 = noexcept(func2()); // false
9.3 noexcept与函数重载
noexcept可作为函数重载的区分条件:
void print(int x) noexcept {
std::cout << "noexcept version: " << x << std::endl;
}
void print(int x) {
std::cout << "throwing version: " << x << std::endl;
}
int main() {
print(42); // 调用哪个版本?(歧义,编译错误)
return 0;
}
9.4 noexcept的优化作用
编译器可对noexcept函数进行优化(如省略异常处理代码):
// 向量移动构造函数在元素类型为noexcept时可优化
std::vector<MyNoexceptType> v1;
std::vector<MyNoexceptType> v2 = std::move(v1); // 高效移动
std::vector<MyThrowingType> v3;
std::vector<MyThrowingType> v4 = std::move(v3); // 可能回退到拷贝
十、异常的优缺点
10.1 优点(相比错误码)
- 分离错误处理与正常逻辑:代码更清晰。
- 强制处理错误:未处理异常会终止程序,避免错误被忽略。
- 传播性:深层调用的错误可被上层统一处理。
- 丰富的错误信息:异常对象可携带详细信息。
10.2 缺点
- 性能开销:异常处理可能增加代码大小和运行时开销。
- 控制流跳转:异常跳转可能使调试困难。
- 异常安全复杂:编写异常安全代码难度高。
- 兼容性:与不使用异常的代码(如C语言)集成困难。
十一、异常处理最佳实践
11.1 只抛出异常对象,不抛出指针
抛出指针需手动管理内存,易导致泄漏:
// 错误:抛出指针
throw new std::runtime_error("error"); // 需手动delete
// 正确:抛出对象
throw std::runtime_error("error"); // 自动管理生命周期
11.2 捕获具体类型,避免过度使用catch(…)
catch(...)会捕获所有异常,但无法获取错误信息,可能隐藏bug:
try {
// ...
} catch (const std::out_of_range& e) { // 捕获具体类型
// 处理越界错误
} catch (const std::exception& e) { // 捕获其他已知异常
// 处理标准异常
} // 不使用catch(...),让未知异常终止程序,便于调试
11.3 使用RAII确保资源释放
RAII是异常安全的基石,避免手动释放资源:
// 错误:手动管理资源
void bad() {
int* ptr = new int;
risky_operation(); // 异常可能导致内存泄漏
delete ptr;
}
// 正确:RAII管理资源
void good() {
std::unique_ptr<int> ptr(new int); // RAII
risky_operation(); // 异常时自动释放
}
11.4 异常规格说明的使用
- C++11前:
throw(type1, type2)指定可能抛出的异常类型(已弃用)。 - C++11后:使用
noexcept指定是否抛出异常。
十二、常见误区与注意事项
12.1 析构函数抛出异常
析构函数抛出异常会导致程序终止,应在析构函数内捕获所有异常:
class SafeClass {
public:
~SafeClass() {
try {
risky_cleanup(); // 可能抛出异常的清理操作
} catch (...) {
// 记录错误,不传播异常
std::cerr << "cleanup failed" << std::endl;
}
}
};
12.2 捕获所有异常(catch(…))的风险
catch(...)会捕获包括std::bad_alloc在内的所有异常,可能导致程序在不可恢复状态下继续运行:
try {
// ...
} catch (...) {
// 危险:未处理根本错误(如内存耗尽)
std::cout << "something went wrong" << std::endl;
}
12.3 异常导致的资源泄漏
未使用RAII的代码在异常抛出时可能泄漏资源:
void leak() {
FILE* file = fopen("data.txt", "r");
if (!file) return;
throw "error"; // 文件未关闭,资源泄漏
fclose(file);
}
12.4 异常与多线程
异常不能跨线程传播,每个线程需独立处理异常:
#include <thread>
void thread_func() {
try {
throw std::runtime_error("thread error");
} catch (const std::exception& e) {
std::cout << "thread caught: " << e.what() << std::endl;
}
}
int main() {
std::thread t(thread_func);
t.join(); // 异常在子线程内处理
return 0;
}
以上内容,全面覆盖了C++异常处理的核心知识点,包括语法、栈展开、异常安全、标准异常库、自定义异常等。合理使用异常可提高代码的健壮性和可读性,但需注意异常安全和最佳实践,避免常见误区。
更多推荐



所有评论(0)