这一章整理异常处理。C 语言通常靠返回值、错误码和宏定位问题;C++ 增加了异常机制,可以把错误沿调用链向上传播。安全开发里异常不是“偷懒不判断错误”,而是要让错误路径可控、资源能释放、日志能复盘。

6.1 C 预定义宏

C/C++ 提供一些预定义宏,适合写日志和调试信息。

含义
__FILE__ 当前文件名
__LINE__ 当前行号
__DATE__ 编译日期
__TIME__ 编译时间
__FUNCTION__ 当前函数名,常见编译器支持
1
2
3
4
#include <iostream>

#define LOG(msg) \
std::cout << __FILE__ << ":" << __LINE__ << " " << msg << std::endl

6.3 C 错误规避

C 风格错误处理常用返回值。

1
2
3
4
5
6
int read_config(const char *path) {
if (path == NULL) {
return 0;
}
return 1;
}

复盘重点:调用者必须检查返回值,否则错误会被吞掉。

6.4 C 标准错误

C 中常见 errno、返回值、空指针判断。文件、内存、系统调用都要检查失败情况。

场景 检查
fopen 是否返回 NULL
malloc 是否返回 NULL
字符串转换 是否转换完整
文件读取 返回数量是否符合预期

6.5 C++ 抛出异常

C++ 可以用 throw 抛出异常,用 try/catch 捕获。

1
2
3
4
5
6
7
8
#include <stdexcept>

int divide(int a, int b) {
if (b == 0) {
throw std::invalid_argument("divide by zero");
}
return a / b;
}

6.6 异常传播

异常会沿调用栈向上传播,直到被匹配的 catch 捕获。如果没人捕获,程序会终止。

1
2
3
4
5
6
try {
int ret = divide(10, 0);
std::cout << ret << std::endl;
} catch (const std::exception &e) {
std::cerr << e.what() << std::endl;
}

6.7 异常规范

旧式动态异常规范已经不推荐。现代 C++ 更常见的是 noexcept,表示函数承诺不抛异常。

1
2
3
void cleanup() noexcept {
// 清理函数不应抛异常
}

析构函数里尤其不要抛异常。

6.8 标准异常

常见标准异常:

异常 场景
std::exception 标准异常基类
std::runtime_error 运行时错误
std::invalid_argument 参数非法
std::out_of_range 越界
std::bad_alloc 内存申请失败

6.9 自定义异常

自定义异常适合表达项目自己的错误语义。

1
2
3
4
5
6
7
8
#include <stdexcept>
#include <string>

class ConfigError : public std::runtime_error {
public:
explicit ConfigError(const std::string &message)
: std::runtime_error(message) {}
};

异常安全与资源释放

异常处理最怕资源泄漏。C++ 里优先用 RAII:资源放进对象,析构时自动释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <fstream>
#include <stdexcept>
#include <string>

std::string read_first_line(const std::string &path) {
std::ifstream file(path);
if (!file) {
throw std::runtime_error("open file failed");
}

std::string line;
std::getline(file, line);
return line;
}

ifstream 离开作用域会自动关闭文件。

错误处理选择表

方式 适合场景
返回值 高频、可预期、简单错误
errno C 标准库和系统调用风格
异常 无法在当前层处理,需要向上传播
日志 需要保留复盘证据
RAII 资源必须自动释放

安全复盘问题

问题 说明
是否检查失败 文件、内存、输入都要检查
是否泄漏资源 异常路径也要释放
是否吞掉异常 catch 会隐藏问题
是否泄露敏感信息 错误信息不要包含密码、密钥
是否有日志 关键错误要能复盘

本章最小复盘代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <stdexcept>
#include <string>

int parse_positive(const std::string &text) {
int value = std::stoi(text);
if (value <= 0) {
throw std::invalid_argument("value must be positive");
}
return value;
}

int main() {
try {
std::cout << parse_positive("12") << std::endl;
std::cout << parse_positive("-1") << std::endl;
} catch (const std::exception &e) {
std::cerr << "error: " << e.what() << std::endl;
}
return 0;
}

学习检查清单

检查项 状态
能用预定义宏输出文件名和行号 待复盘
能写出返回值式错误处理 待复盘
能抛出并捕获标准异常 待复盘
能说明异常传播和 catch 匹配顺序 待复盘
能用 RAII 避免异常路径资源泄漏 待复盘