这一章在干嘛?
stdio 的体系:错误报告与终止、流的概念与 fopen/fclose、字符与行 I/O、scanf/printf 的格式暗号、二进制 I/O 与随机定位。学会本章,程序就能与文件世界打交道。
15.4 格式化 I/O:scanf / printf 的暗号表
15.1 错误报告与终止执行
#include <stdio.h>
#include <stdlib.h>
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("data.txt"); /* 打印 "data.txt: <errno描述>",自动带上原因 */
exit(EXIT_FAILURE); /* 立即终止;EXIT_SUCCESS 表示成功 */
}
perror 把「你给的前缀 + 全局 errno 对应的系统错误描述」一行打出来;exit 直接结束进程并刷新所有打开的流。错误处理的原则:每个可能失败的调用都检查,失败就带上下文报告——「函数名+文件名+errno」三件套让排错事半功倍。
15.2 流:打开、关闭与三种标准流
**流(stream)*是对「数据源/目的地」的抽象:文件、终端、管道在程序眼里都是 FILE。打开时选模式:
| 模式 | 含义 | 文件不存在 | 已存在 |
|---|---|---|---|
| ”r” | 只读 | 失败 NULL | 从头读 |
| ”w” | 只写 | 新建 | 清空! |
| “a” | 追加 | 新建 | 从尾写 |
| ”r+“ | 读+写 | 失败 NULL | 从头 |
| ”w+b” 等加 b | 二进制模式 | — | 不换行翻译 |
程序启动自带三个流:stdin(标准输入)、stdout(标准输出)、stderr(标准错误),不用 fopen 就能用。stderr 不缓冲,错误信息优先于 stdout 显示——报告错误请用它。
fclose 别忘配对:
每个成功 fopen 都要 fclose,否则文件句柄泄漏。fclose 也会把缓冲区写完,关之前别丢 FILE*。
15.3 字符与行 I/O
| 家族 | 读 | 写 | 撤销 |
|---|---|---|---|
| 字符 | fgetc(fp) / getc / getchar | fputc(ch, fp) / putchar | ungetc(ch, fp) |
| 行 | fgets(buf, n, fp) / gets(禁用!无边界) | fputs(line, fp) / puts | — |
/* 字符级复制:EOF 是「流结束」信号,类型必须是 int! */
int ch;
while ((ch = fgetc(src)) != EOF)
fputc(ch, dst);
/* 行级读取的安全姿势:fgets 自带截断 + 保留换行 */
char line[256];
while (fgets(line, sizeof line, fp) != NULL) {
line[strcspn(line, "\n")] = '\0'; /* 去掉末尾换行(若有) */
process(line);
}
为什么 ch 用 int:
fgetc 返回「字符值或 EOF」。char 若无符号将永远等于不了 EOF(-1);用 int 接收才能正确区分「数据 0xFF」和「文件结束」。
15.4 格式化 I/O:scanf / printf 的暗号表
| 转换符 | printf 输出 | scanf 读取 |
|---|---|---|
| %d / %u | 有符号 / 无符号十进制 | int* / unsigned* |
| %x / %o | 十六进制 / 八进制 | 同上 |
| %c / %s | 字符 / 字符串 | char*(s 要留 \0 空间!) |
| %f %e %g | 浮点三种风格 | float*;double 用 %lf |
| %ld %lld %zu | long / long long / size_t | 对应指针类型 |
| %p %% | 指针地址 / 百分号本身 | void** / — |
printf 修饰符:%5d(宽度5右对齐)、%-5d(左对齐)、%05d(补零)、%.2f(两位小数)、%*d(宽度由参数给)——第 1 章程序里的 %.*s(精度取自参数的字符串截取)就是这套体系。
scanf 三要害:① 参数必须是指针(&n,字符串数组名本来就退化成指针不用 &);② %s 停在空白且不限长(用 %63s 指定域宽防溢出);③ 返回成功匹配的项目数,用它判断输入是否合法:
int n;
if (scanf("%d", &n) != 1) { /* 输入 abc 时返回 0 */
fprintf(stderr, "不是数字\n");
exit(EXIT_FAILURE);
}
15.5 二进制 I/O 与随机定位
struct Rec { int id; double score; };
struct Rec r = { 7, 93.5 };
FILE *fp = fopen("rec.bin", "wb"); /* 二进制模式 */
fwrite(&r, sizeof r, 1, fp); /* 整块写入 */
fclose(fp);
fp = fopen("rec.bin", "rb");
fseek(fp, 1 * sizeof(struct Rec), SEEK_SET); /* 跳到第 2 条记录 */
fread(&r, sizeof r, 1, fp); /* 随机读取 */
long pos = ftell(fp); /* 当前偏移 */
rewind(fp); /* 回到开头 */
fclose(fp);
文本 vs 二进制的取舍:文本可读、可移植、易调试,但要转换且占空间;二进制紧凑、免转换、可随机访问,但受字节序/结构填充影响,跨平台要小心。结构体数组、帧数据用二进制整块读写最快。
本章通关标准:
fopen 之后条件反射判 NULL;fgets + strcspn 去 \n 的模板随手能写;scanf 返回值必检查;理解 fseek 三种基准(SEEK_SET/CUR/END)。
1. fgetc 的返回值为什么必须是 int 接收?
返回「字符值或 EOF(-1)」。若用 char(尤其无符号实现)接收,-1 被截断成 255,与 EOF 比较永远不相等,文件结束判断失效甚至死循环。int 才能同时容纳所有字符值与 EOF。
2. %s 在 scanf 里有什么风险?怎么防?
%s 遇空白停止但默认不限长度,输入超长会溢出目标缓冲区。防护:指定域宽 %63s(为 \0 留 1 字节);或改用 fgets 读整行再解析。另外 scanf 的 %s 参数本身就是指针(数组名),不要再加 &。
3. 二进制 I/O 相比文本 I/O 的优缺点?
优点:免格式转换、占空间小、fseek+fread 可随机访问整块数据,适合结构体/帧数据。缺点:文件不可直接阅读;受平台字节序与结构填充影响,跨平台交换需要序列化约定。