本节摘要:C 的错误处理哲学是"返回错误码加 errno 细节",语言不强制检查,纪律决定质量。本节串起 perror/errno/assert 的分层防御、段错误的排查流程、gdb 与日志的实战用法,最后落到命名、注释、模块化的工程规范——让代码从"能跑"变成"团队敢改"。
C 库函数的统一约定:失败返回哨兵值(NULL、-1、EOF 等),细节写进全局 errno。errno 是一个线程安全的错误码变量(宏展开成线程局部存储),每次调用库函数后有效、随时会被下一次调用覆盖——先存后用:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> int read_config(const char *path, char *buf, size_t cap) { FILE *fp = fopen(path, "rb"); if (fp == NULL) { int saved = errno; /* 先保存,防止被覆盖 */ snprintf(buf, cap, "打开配置失败: %s", strerror(saved)); return -1; } size_t n = fread(buf, 1, cap - 1, fp); if (ferror(fp)) { snprintf(buf, cap, "读取失败: %s", strerror(errno)); fclose(fp); return -1; } buf[n] = '\0'; fclose(fp); return 0; }
strerror 把错误码翻成人类句子;perror 是"打印前缀加当前 errno 描述"的快捷方式。错误码分层:函数内部错误要转成自己的错误码向上传,或在日志里留痕——静默吞错误是 C 项目的头号恶习,出错没人知道,排查时一片漆黑。
#include <assert.h> int divide(int a, int b) { assert(b != 0); /* 开发期:条件为假立即崩溃并打印表达式与行号 */ return a / b; }
assert 的价值是"把假设写在代码里":入参契约、循环不变量、不可能到达的分支。失败时程序当场中止,打印表达式、文件、行号——比"带着错误数据继续跑出诡异结果"好一万倍。两个纪律:
assert(close(fp) == 0) 在发布版里压根不会执行。errno 处理"运行期现实",assert 处理"逻辑漏洞",两个分层各守一边。
全书见过多次段错误,现在给出标准流程:
第一步 用调试器跑 拿到崩溃点与调用栈 gdb 跑程序 崩溃后 bt 看调用链 info locals 看现场 第二步 看崩溃行 问三个问题 指针是 NULL 吗(空指针解引用) 下标越界了吗(检查边界变量) 对象还活着吗(悬垂:已返回栈帧 已释放堆块) 第三步 无法复现或现场被破坏时 上重型工具 地址消毒器:越界与释放后使用当场报错带堆栈 valgrind:泄漏与越界的全面体检
日志打点注意第 2 章的教训:用 stderr(默认不缓冲)或打点后 fflush,否则崩溃前的日志还躺在缓冲区里,误导排查方向。
命名:见名知义、风格统一。变量与函数用小写下划线(max_count、parse_config)、宏与常量全大写(MAX_USERS)、类型加后缀或前缀统一(node_t、struct node)。函数名动词开头(get_、parse_、free_),配对操作命名对仗(open/close、alloc/free、lock/unlock)——命名是最低成本的文档。
注释:写"为什么",不写"是什么"。i++; /* i 加一 */ 是负资产;/* 传感器数据为 0 表示断线,跳过本帧 */ 是正资产。函数头注释写契约:参数含义、返回值、错误码、谁负责释放返回的内存(第 5 章所有权问题的落点)。
模块化:一个模块一对文件(头放接口、源放实现),头文件即模块的公开文档;内部函数加 static 收编为私有(第 1 章 nm 小写符号的实践);依赖方向单向,禁止双向依赖;不透明指针隐藏实现细节(FILE 模式)。模块边界就是责任边界——改动不扩散,是团队协作的物理基础。
/* 队列模块的头文件:一个五脏俱全的最小接口示例 */ #ifndef QUEUE_H #define QUEUE_H typedef struct queue queue; /* 不透明:调用者看不到内部 */ queue *queue_create(void); /* 返回 NULL 表示失败 */ int queue_push(queue *q, int v); /* 0 成功 -1 失败 */ int queue_pop(queue *q, int *out);/* 0 成功 -1 空 */ void queue_destroy(queue *q); /* 与 create 配对 */ #endif
这个 30 行的头文件体现了全部规范:类型不透明、命名对仗、契约清晰、配对完整。任何团队成员拿到它都能正确使用队列,而不需要读实现。

⚠️ 常见坑:把编译警告当噪音。C 的警告几乎全是真实问题的预告——隐式转换、未初始化、返回值未检查、格式串不匹配。项目从第一天就把警告全开并当错误处理,成本远低于事后清还欠债。
💡 关键直觉:错误处理的三层各有辖区——assert 守开发期逻辑、错误码守运行期现实、调试器守崩溃现场。三层都到位的程序,出问题时线索完整;靠运气的程序,出问题时一片漆黑。
全书到这里收束。从一条构建命令的流水线,到进程内存的分区地图,到指针与栈帧的字节级真相,再到贴近硬件的位与文件——机器视角的每一层都已经打开。剩下的路在键盘上:把教程里的代码敲进机器,用调试器单步走过,亲手制造并修复一次段错误。对机器的直觉,从来都是练出来的。