【C++】异常--学习笔记
目录
- 一:异常的概念
- 二:异常的抛出于捕获
- 三:栈展开
- 四:查找匹配的处理代码
- 1. 完全匹配
- 2. 常量转换
- 3. 派生类 → 基类
- 4. 未被捕获 → terminate,程序会直接调用:std::terminate();
- catch(...) 万能捕获
- 五:异常重新抛出
- throw e; 与 throw; 的区别:
- 六:异常安全问题
- 七:异常规范
一:异常的概念
- 异常处理机制允许程序独立开发的部分能够在运行时出现的问题进行通信并做出相应的处理。
- 在C语言中,主要通过错误码的形式处理错误,错误码绷直就是对错误信息进行分类编号,但是在拿到错误码之后还要查询错误信息,比较麻烦。
- 异常可以抛出一个对象,这个对象可以让我们知道更加 全面的信息。
二:异常的抛出于捕获
- 在程序出现问题的时候,通过抛出(throw)⼀个对象来引发⼀个异常,然后就会沿着调用链catch来处理该异常。
- 被选中的处理代码是调⽤链中与该对象类型匹配且离抛出异常位置最近的那⼀个。
- 当throw执⾏时,throw后⾯的语句将不再被执⾏
- 程序的执⾏从throw位置跳到与之匹配的catch模块,catch可能是同⼀函数中的⼀个局部的catch,也可能是调⽤链中另⼀个函数中的catch,控制权从throw位置转移到了catch位置。
- 沿着调⽤链的函数可能提早退出。
- ⼀旦程序开始执⾏异常处理程序,沿着调⽤链创建的对象都将销毁。
-
抛出异常对象后,会⽣成⼀个异常对象的拷⻉,因为抛出的异常对象可能是⼀个局部对象,所以会⽣成⼀个拷⻉对象,这个拷⻉的对象会在catch⼦句后销毁。
-
核心关键词:
- try:包围可能抛出异常的代码块。
- throw:抛出异常。
- catch:捕获异常。
- noexcept(C++11 新增):声明函数不会抛出异常,有助于优化和安全性。
例子如下:
#include <iostream>
#include <stdexcept>void test(int x) {if (x == 0) {throw std::runtime_error("x不能为0");}std::cout << "x = " << x << std::endl;
}int main() {try {test(0);} catch (const std::exception& e) {std::cout << "捕获异常: " << e.what() << std::endl;}
}
三:栈展开
- 抛出异常后,程序暂停当前函数的执⾏,开始寻找与之匹配的catch⼦句,⾸先检查throw本⾝是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地⽅进⾏处理。
- 如果当前函数中没有try/catch⼦句,或者有try/catch⼦句但是类型不匹配,则退出当前函数,继续在外层调⽤函数链中查找,上述查找的catch过程被称为栈展开。
- 如果到达main函数,依旧没有找到匹配的catch⼦句,程序会调⽤标准库的 terminate 函数终⽌程序。
- 如果找到匹配的catch⼦句处理后,catch⼦句代码会继续执⾏。
用例代码:
#include <iostream>
#include <stdexcept>
using namespace std;void func3() {cout << "进入 func3\n";throw runtime_error("错误发生在 func3");cout << "退出 func3\n"; // 不会执行
}void func2() {cout << "进入 func2\n";func3();cout << "退出 func2\n"; // 不会执行
}void func1() {cout << "进入 func1\n";try {func2();} catch (const runtime_error& e) {cout << "捕获异常: " << e.what() << endl;}cout << "退出 func1\n";
}int main() {func1();
}
注意:
- 不能在析构函数中抛异常,
因为析构函数是在栈展开期间自动调用的,如果在析构函数里再抛异常,会导致“双重异常”,程序立刻调用std::terminate() 崩溃。
struct Bad {~Bad() noexcept(false) { throw std::runtime_error("析构又抛了"); }
};
- 局部对象自动销毁,堆对象不会,所以一定要配合智能指针使用堆对象,否则内存泄漏。
void f() {int* p = new int(42);throw std::runtime_error("异常");delete p; // 永远不会执行!
}
四:查找匹配的处理代码
- ⼀般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个。
- 但是也有⼀些例外,允许从⾮常量向常量的类型转换,也就是权限缩⼩;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派⽣类向基类类型的转换,这个点⾮常实⽤,实际中继承体系基本都是⽤这个⽅式设计的。
- 如果到main函数,异常仍旧没有被匹配就会终⽌程序,不是发⽣严重错误的情况下,我们是不期望程序终⽌的,所以⼀般main函数中最后都会使⽤catch(…),它可以捕获任意类型的异常,但是是不知道异常错误是什么。
匹配类型 | 说明 |
---|---|
完全匹配 | throw int; → catch (int) |
常量转换 | throw int; → catch (const int) (非 const 转 const) |
数组 → 指针转换 | throw int[3]; → catch (int*) |
函数 → 指针转换 | throw void f(); → catch (void(*)()) |
派生类 → 基类转换 | throw Derived(); → catch (Base&) |
用例代码:
1. 完全匹配
try {throw 42;
} catch (int x) {std::cout << "捕获 int: " << x << std::endl;
}
输出:
捕获 int: 42
2. 常量转换
try
{throw 42;
}
catch (const int& x)
{ //允许从 int 到 const int&std::cout << "捕获 const int&: " << x << std::endl;
}
输出:
捕获 const int&: 42
3. 派生类 → 基类
#include <iostream>
#include <stdexcept>
using namespace std;struct Base { virtual ~Base() {} };
struct Derived : Base {};int main() {try {throw Derived();} catch (const Derived&) {cout << "捕获 Derived\n";} catch (const Base&) {cout << "捕获 Base\n";}
}
输出:
捕获 Derived
4. 未被捕获 → terminate,程序会直接调用:std::terminate();
void f() {throw std::runtime_error("错误");
}int main() {f(); // 没有 try-catch 包裹
}
- 这是编译器在找不到任何匹配的 catch 时自动触发的。它的默认行为是调用 std::abort(),程序立刻结束。
catch(…) 万能捕获
try {throw 3.14;
} catch (...) {std::cout << "捕获未知类型异常\n";
}
输出:
捕获未知类型异常
五:异常重新抛出
- 有时catch到⼀个异常对象后,需要对错误进⾏分类,其中的某种异常错误需要进⾏特殊的处理,其他错误则重新抛出异常给外层调⽤链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对象直接抛出。
- 在 C++ 中,重新抛出异常(rethrow) 通常用于这种场景:
你在 catch 块里捕获了一个异常,做了一些局部处理(比如记录日志、释放资源、打印信息等),但你希望外层调用者继续知道这个异常,于是你就“重新扔出去”。 - 基本使用方法:
try {func();
} catch (const std::exception& e) {std::cerr << "日志: 捕获到异常: " << e.what() << std::endl;throw; //重新抛出同一个异常对象
}
注意:
- 这里的 throw; 没有对象名,表示“把当前捕获的异常重新抛出”
throw e; 与 throw; 的区别:
- throw
抛出的是当前捕获的同一个异常对象(不重新构造)。
不会切断异常链,也不会复制。
通常用来保留异常的原始信息(类型、what() 等)。
throw e;
抛出的是一个新的异常副本,会调用拷贝构造函数。
如果捕获类型是基类引用(比如 const std::exception&),那么再 throw e; 会发生对象切片。导致原本派生类的异常信息被截断,只剩下基类部分。
例如:
struct MyError : public std::exception {const char* what() const noexcept override { return "MyError"; }
};try {throw MyError();
} catch (const std::exception& e) {throw e; //这里会丢失原始类型信息,只剩 std::exception
}
而如果写成:
catch (const std::exception& e) {throw; // 保留原始异常类型 MyError
}
外层依然能 catch (const MyError&) 成功。
再举个复杂点的例子:
void level3() {throw std::runtime_error("底层错误");
}void level2() {try {level3();} catch (...) {std::cerr << "level2 日志: 捕获到异常\n";throw; // 重新抛出,让上层也能处理}
}void level1() {try {level2();} catch (const std::exception& e) {std::cerr << "level1 处理: " << e.what() << '\n';}
}int main() {level1();
}
输出会是:
level2 日志: 捕获到异常
level1 处理: 底层错误
- 你能看到,异常在中途被记录,但最终还是传到了顶层去处理。
- 这就像接力赛中,中间选手稍微看了一眼接力棒,但还是把它继续传了出去。
六:异常安全问题
- 之前我们说到,在 C++ 里,一旦抛出异常,控制流会立刻中断当前作用域后续的代码执行。
- 这时候,就会引出问题,假如有申请的资源未释放,不就在内存泄漏了吗?所以我们就需要智能指针来解决这个问题。
void f() {int* p = new int[10]; // 申请资源g(); // g() 可能抛异常delete[] p; // 永远执行不到这里
}
g() 一旦抛出异常,delete[] p 就不执行,内存泄漏。
同理,如果你在异常前加锁、打开文件、申请句柄,这些资源都会“悬空”。
解决办法1:手动 try-catch + rethrow
void f() {int* p = new int[10];try {g();} catch (...) {delete[] p; // 释放资源throw; // 重新抛出异常给上层}delete[] p;
}
*这样做逻辑正确,但写起来太容易出错,而且每一个资源都得记得包裹 try-catch。
- 人一多、代码一长,出事概率接近 100%。
解决办法2:RAII(Resource Acquisition Is Initialization)
RAII 是 C++ 异常安全的灵魂。
核心思想:资源绑定到对象生命周期上。
对象离开作用域(包括异常跳出)时自动释放资源。
#include <memory>void f() {std::unique_ptr<int[]> p(new int[10]); // 自动管理g(); // 如果这里抛异常,p 的析构函数自动 delete[]
}
RAII 适用于所有“需要释放”的资源:内存、锁、文件、socket、数据库连接。
七:异常规范
• 对于⽤⼾和编译器⽽⾔,预先知道某个程序会不会抛出异常⼤有裨益,知道某个函数是否会抛出异常有助于简化调⽤函数的代码。
• C++98中函数参数列表的后⾯接throw(),表⽰函数不抛异常,函数参数列表的后⾯接throw(类型1,类型2…)表⽰可能会抛出多种类型的异常,可能会抛出的类型⽤逗号分割。
• C++98的⽅式这种⽅式过于复杂,实践中并不好⽤,C++11中进⾏了简化,函数参数列表后⾯加noexcept表⽰不会抛出异常,啥都不加表⽰可能会抛出异常。
• 编译器并不会在编译时检查noexcept,也就是说如果⼀个函数⽤noexcept修饰了,但是同时⼜包含了throw语句或者调⽤的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是⼀个声明noexcept的函数抛出了异常,程序会调⽤ terminate 终⽌程序。
• noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,可能会则返回false,不会就返回true。
- C++11 的新语法:noexcept
C++11 提出更简洁的替代方案:
void f() noexcept; // 表示不会抛异常
void g(); // 可能抛异常
语义:
写了 noexcept:承诺“我绝不抛异常”。
没写:默认“我可能抛”。
比起 throw(),noexcept 不再关注“会抛什么类型”,而是只关心“会不会抛”。
规则:
如果一个 noexcept 函数里真的抛出异常,会触发 std::terminate()。
程序直接终止,异常不会被外层 catch 捕获。
举个例子:
void f() noexcept {throw std::runtime_error("oops");
}int main() {try {f();} catch (...) {std::cout << "捕获不到!" << std::endl;}
}
结果:程序崩溃。
因为异常从 noexcept 函数中逃出,违反了承诺。
- 需要注意的是,noexcept 不会强制检查(但可以警告)
编译器不会阻止你在 noexcept 函数里写 throw,它只会在运行时触发 terminate()。
所以这个修饰符更像是一种契约(contract),告诉调用者“这里安全,不会抛”。
部分编译器(如 Clang、MSVC)会给出警告,比如:
warning: exception thrown from noexcept function will terminate program