错误处理
C 语言没有内建的异常机制 (Exception)。在实践中,错误处理通常依赖以下三类工具:
- 断言:用
assert在调试阶段暴露“本不该发生”的逻辑错误。 - 错误码:通过函数返回值表达失败,再用
errno(或自定义错误码)补充失败原因。 - 信号:处理异步中断(如
SIGINT),或理解进程因严重错误而终止的路径(如SIGSEGV)。
本章只讨论 C 标准库提供的三组能力:<assert.h>、<errno.h>、<signal.h>。
1. 何时使用哪一种
| 场景 | 推荐手段 | 目的 |
|---|---|---|
| 代码内部的恒定约束被破坏(逻辑 bug) | assert | 尽快在开发阶段失败并定位问题。 |
| 外部输入/环境导致的失败(用户输入、文件不存在、资源不足等) | 返回值 + errno(或自定义错误码) | 让调用者有机会处理或上报错误。 |
| 异步事件(如 Ctrl+C)或运行时致命错误 | signal | 做最小化的收尾(或记录)后退出;不把它当作“正常控制流”。 |
重要
不要把 assert 当作“运行时的错误处理”。定义了 NDEBUG 后,assert 可能会被完全移除,表达式也不会被求值。
2. 本章目录
3. 错误路径要形成统一出口
错误处理的难点常常不在“发现失败”,而在“失败后如何稳定收束”。一个常见且有效的做法是:函数内部先用返回值逐层上传错误,再在模块边界统一记录日志、映射错误码和决定是否终止。这样既能保留细粒度原因,也能避免同一错误在多层重复处理。
4. 可恢复错误与不可恢复错误分层
输入格式错误、资源暂时不足通常属于可恢复错误,应优先走返回值路径;内部状态损坏、关键前提被破坏则更接近不可恢复错误,应尽早中止并保留诊断信息。把这两类失败分层后,assert、错误码与信号处理的职责边界会清晰很多。
5. 习题
- 分别给出一个例子,说明“应该用
assert”与“应该用返回值 +errno”的区别;并解释原因。 - 设计一个函数
int parse_u32(const char* s, uint32_t* out);:- 当
s == NULL或out == NULL时,选择assert还是返回错误?说明理由。 - 当字符串不是合法数字或超出范围时,返回什么错误信息(错误码/
errno)?说明理由。
- 当
- 写一个小程序:循环读取一行输入并处理;当收到
SIGINT(Ctrl+C)时退出。要求:信号处理函数里只设置一个标志位,不做 I/O。