异常处理机制
定义与作用
异常处理是 C++ 的错误传播机制——当函数无法正常完成任务时,throw 一个异常对象,沿调用栈向上传播,直到被匹配的 catch 块捕获:
try {
int result = divide(10, 0); // 可能抛出异常
} catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << std::endl;
}
相比 C 风格的错误码返回,异常处理的优势:
- 错误无法被忽略(不捕获就 terminate)
- 将正常逻辑和错误处理分离
- 支持跨多层函数调用传播
核心原理
栈展开(Stack Unwinding)
std::exception 体系
完整示例
示例一:学生成绩管理系统的异常处理
场景说明:学生成绩录入和管理系统,对非法输入、空数据、越界等情况抛出不同类型异常。
#include <iostream>
#include <vector>
#include <string>
#include <stdexcept>
#include <numeric>
#include <algorithm>
#include <map>
class ScoreDatabase {
public:
void addScore(const std::string& student, int score) {
if (score < 0 || score > 100)
throw std::invalid_argument(
"分数必须在 0~100 之间,实际值: " + std::to_string(score));
if (student.empty())
throw std::invalid_argument("学生姓名不能为空");
scores_[student].push_back(score);
std::cout << " 添加: " << student << " = " << score << std::endl;
}
double average(const std::string& student) const {
auto it = scores_.find(student);
if (it == scores_.end())
throw std::out_of_range("未找到学生: " + student);
const auto& scores = it->second;
if (scores.empty())
throw std::runtime_error(student + " 尚无成绩记录");
double sum = std::accumulate(scores.begin(), scores.end(), 0.0);
return sum / scores.size();
}
int highest(const std::string& student) const {
auto it = scores_.find(student);
if (it == scores_.end())
throw std::out_of_range("未找到学生: " + student);
const auto& scores = it->second;
if (scores.empty())
throw std::runtime_error(student + " 尚无成绩记录");
return *std::max_element(scores.begin(), scores.end());
}
void printAll() const {
if (scores_.empty()) {
std::cout << " 数据库中无数据" << std::endl;
return;
}
for (const auto& [name, scores] : scores_) {
std::cout << " " << name << ": ";
for (size_t i = 0; i < scores.size(); ++i) {
if (i > 0) std::cout << ", ";
std::cout << scores[i];
}
std::cout << " (均分: " << average(name) << ")" << std::endl;
}
}
private:
std::map<std::string, std::vector<int>> scores_;
};
int main() {
ScoreDatabase db;
std::cout << "===== 正常录入 =====" << std::endl;
try {
db.addScore("张三", 85);
db.addScore("张三", 92);
db.addScore("李四", 78);
db.addScore("李四", 88);
db.addScore("李四", 95);
} catch (const std::exception& e) {
std::cerr << "添加失败: " << e.what() << std::endl;
}
std::cout << "\n===== 查询成绩 =====" << std::endl;
db.printAll();
std::cout << "\n===== 异常场景 =====" << std::endl;
// 场景 1:非法分数
try {
db.addScore("王五", 150);
} catch (const std::invalid_argument& e) {
std::cerr << "非法参数: " << e.what() << std::endl;
}
// 场景 2:查询不存在的学生
try {
db.average("赵六");
} catch (const std::out_of_range& e) {
std::cerr << "越界: " << e.what() << std::endl;
}
// 场景 3:统捕获所有标准异常
try {
std::vector<int> v;
v.at(100); // 抛出 std::out_of_range
} catch (const std::logic_error& e) {
std::cerr << "逻辑错误: " << e.what() << std::endl;
} catch (const std::runtime_error& e) {
std::cerr << "运行时错误: " << e.what() << std::endl;
} catch (const std::exception& e) {
std::cerr << "其他异常: " << e.what() << std::endl;
}
return 0;
}
预期输出:
===== 正常录入 =====
添加: 张三 = 85
添加: 张三 = 92
添加: 李四 = 78
添加: 李四 = 88
添加: 李四 = 95
===== 查询成绩 =====
张三: 85, 92 (均分: 88.5)
李四: 78, 88, 95 (均分: 87)
===== 异常场景 =====
非法参数: 分数必须在 0~100 之间,实际值: 150
越界: 未找到学生: 赵六
逻辑错误: invalid vector subscript
逐段分析:
throw抛出异常对象——编译器根据静态类型创建副本catch (const std::invalid_argument& e)按异常类型匹配——推荐用 const 引用避免切片- 多个 catch 块按书写顺序匹配——派生类异常必须写在基类异常前面
- 栈展开保证局部对象正确析构——RAII 资源自动释放
std::exception::what()返回描述信息的 C 字符串
示例二:文件处理中的异常安全
#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>
#include <vector>
#include <sstream>
class ConfigFile {
public:
explicit ConfigFile(const std::string& path) {
std::ifstream file(path);
if (!file.is_open())
throw std::runtime_error("无法打开配置文件: " + path);
std::string line;
int lineNum = 0;
while (std::getline(file, line)) {
++lineNum;
if (line.empty() || line[0] == '#') continue; // 跳空行和注释
auto pos = line.find('=');
if (pos == std::string::npos) {
file.close();
throw std::runtime_error(
"配置文件格式错误 (第" + std::to_string(lineNum) + "行)");
}
std::string key = trim(line.substr(0, pos));
std::string value = trim(line.substr(pos + 1));
if (key.empty())
throw std::invalid_argument("第" + std::to_string(lineNum) + "行: 键名为空");
entries_.push_back({key, value});
}
}
std::string get(const std::string& key) const {
for (const auto& [k, v] : entries_) {
if (k == key) return v;
}
throw std::out_of_range("配置项不存在: " + key);
}
void print() const {
for (const auto& [k, v] : entries_)
std::cout << " " << k << " = " << v << std::endl;
}
private:
static std::string trim(const std::string& s) {
auto start = s.find_first_not_of(" \t");
if (start == std::string::npos) return "";
auto end = s.find_last_not_of(" \t");
return s.substr(start, end - start + 1);
}
std::vector<std::pair<std::string, std::string>> entries_;
};
int main() {
std::cout << "===== 读取正常配置 =====" << std::endl;
try {
// 先创建一个测试配置文件
{
std::ofstream out("test_config.ini");
out << "# 服务器配置\n";
out << "host = 127.0.0.1\n";
out << "port = 8080\n";
out << "timeout = 30\n";
}
ConfigFile cfg("test_config.ini");
cfg.print();
std::cout << "host: " << cfg.get("host") << std::endl;
std::cout << "port: " << cfg.get("port") << std::endl;
} catch (const std::exception& e) {
std::cerr << "配置错误: " << e.what() << std::endl;
}
std::cout << "\n===== 异常场景 =====" << std::endl;
// 场景 1:文件不存在
try {
ConfigFile cfg("nonexistent.ini");
} catch (const std::exception& e) {
std::cerr << "捕获: " << e.what() << std::endl;
}
// 场景 2:查询不存在的键
try {
ConfigFile cfg("test_config.ini");
std::cout << cfg.get("username") << std::endl;
} catch (const std::out_of_range& e) {
std::cerr << "捕获: " << e.what() << std::endl;
}
return 0;
}
预期输出:
===== 读取正常配置 =====
host = 127.0.0.1
port = 8080
timeout = 30
host: 127.0.0.1
port: 8080
===== 异常场景 =====
捕获: 无法打开配置文件: nonexistent.ini
捕获: 配置项不存在: username
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 按值捕获异常 | 对象切片(派生类信息丢失) | 用 const 引用捕获 |
| 派生类异常写在基类之后 | 派生类异常永远不会被匹配 | 派生类 catch 在前 |
| 析构函数中抛出异常 | std::terminate 调用 | 析构函数必须 noexcept |
| 构造函数中抛出异常 | 已构造的成员未析构 | 已构造的成员自动析构,但裸指针管理的资源需手动处理 |
| 异常对象用原生指针 | 内存泄漏 | throw 值对象,catch const 引用 |
常见面试问题
异常处理的栈展开过程?——从 throw 点到匹配 catch 之间,依次销毁所有局部自动对象(RAII),直到找到匹配的 catch 块。如果没找到 catch,调用
std::terminate。为什么 catch 块中应该用 const 引用?——(1) 避免异常对象的拷贝;(2) 如果按值捕获,派生类异常对象会被切片成基类对象,丢失派生类信息。
构造函数中可以抛出异常吗?析构函数呢?——构造函数可以抛出异常,已成功构造的成员和基类会自动析构。析构函数不应该抛出异常(C++11 起默认 noexcept),否则在栈展开时析构函数抛异常会导致
std::terminate。std::exception::what()的作用?——返回异常描述的 C 风格字符串。自定义异常类应 override 此方法。noexcept 标记的函数抛了异常会怎样?——直接调用
std::terminate终止程序,不会进行栈展开。
小结
throw抛出异常对象 → 栈展开 →catch匹配类型捕获- 用 const 引用捕获——避免切片
- 派生类异常 catch 块必须在基类之前
- 析构函数默认 noexcept——绝不要在析构函数中抛异常
- 标准异常体系:
std::exception→logic_error/runtime_error→ 更具体子类