C 语言文件操作笔记(File I/O)
适用:C 语言入门、CS50。头文件:
#include <stdio.h>。
1. 基本概念
C 使用 文件流(stream) 读写文件,通过 FILE * 操作。FILE * 是指向标准库文件流对象的指针,不是指向磁盘内容的 char *。
标准流程:fopen() 打开 → 读写 → fclose() 关闭。
#include <stdio.h>
int main(void)
{
FILE *fp = fopen("hello.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
if (fprintf(fp, "Hello, world!\n") < 0)
{
perror("fprintf");
fclose(fp);
return 1;
}
if (fclose(fp) == EOF)
{
perror("fclose");
return 1;
}
return 0;
}
相对文件路径相对于程序运行时的当前工作目录,不一定是源代码所在目录。
2. fopen():打开文件
FILE *fopen(const char *filename, const char *mode);
成功返回 FILE *;失败返回 NULL。
| 模式 | 读 | 写 | 文件不存在 | 已有内容 |
|---|---|---|---|---|
"r" |
✓ | — | 失败 | 保留 |
"w" |
— | ✓ | 创建 | 清空 |
"a" |
— | ✓(追加) | 创建 | 保留 |
"r+" |
✓ | ✓ | 失败 | 保留 |
"w+" |
✓ | ✓ | 创建 | 清空 |
"a+" |
✓ | ✓(追加) | 创建 | 保留 |
- 后缀
b:二进制模式,例如"rb"、"wb";在部分系统上影响换行转换。 +:允许读写;同一个更新流上交替读写时通常需要适当调用定位或刷新函数,不能直接任意切换。"w"/"w+"打开已有文件时立即截断文件,请谨慎。
FILE *fp = fopen("data.txt", "r");
if (fp == NULL)
{
perror("Cannot open data.txt");
return 1;
}
3. 文本文件写入
| 函数 | 用途 | 示例 |
|---|---|---|
fputc(c, fp) |
写一个字符 | fputc('A', fp); |
fputs(s, fp) |
写字符串 | fputs("Hello\n", fp); |
fprintf(fp, fmt, ...) |
格式化写入 | fprintf(fp, "%d\n", 42); |
FILE *fp = fopen("scores.txt", "w");
if (fp == NULL) return 1;
fputc('A', fp);
fputs("lice ", fp); // 不会自动补换行
fprintf(fp, "%d\n", 95);
if (fclose(fp) == EOF) return 1;
printf(...) 面向标准输出 stdout,而 fprintf(fp, ...) 面向指定流。
4. 文本文件读取
4.1 fgetc():逐字符读取
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) return 1;
int c;
while ((c = fgetc(fp)) != EOF)
{
putchar(c);
}
if (ferror(fp))
{
perror("fgetc");
fclose(fp);
return 1;
}
fclose(fp);
为什么用 int c? fgetc() 要同时表示有效字符和特殊值 EOF,使用 char 可能无法可靠区分。
EOF 不是文件末尾真实存在的字符;它是读取函数用于表示文件结束或出错的返回值。
4.2 fgets():读取一行(或一部分)
char buffer[100];
while (fgets(buffer, sizeof(buffer), fp) != NULL)
{
printf("%s", buffer);
}
- 最多读取
sizeof(buffer) - 1个字符,留下一个位置放\0。 - 如读到换行符且空间足够,会保留
\n。 - 一行过长时,可能需要多次
fgets()才能读完。
4.3 fscanf():格式化读取
假设文件内容:
Alice 90
Bob 85
char name[50];
int score;
while (fscanf(fp, "%49s %d", name, &score) == 2)
{
printf("%s: %d\n", name, score);
}
fscanf() 返回成功赋值的字段个数;%49s 防止将超过容量的字符串写入 name[50]。%s 遇空白字符即停止,不能读取包含空格的完整姓名。
5. 文件位置:ftell、fseek、rewind
| 函数 | 作用 |
|---|---|
ftell(fp) |
获取当前位置标记,失败为 -1L |
fseek(fp, offset, origin) |
修改当前位置,成功返回 0 |
rewind(fp) |
回到文件开头,同时清除错误及 EOF 状态标志 |
fseek() 的参考位置:
SEEK_SET:文件开头SEEK_CUR:当前位置SEEK_END:文件末尾
二进制文件示例:
fseek(fp, 0, SEEK_SET); // 开头
fseek(fp, 10, SEEK_SET); // 从开头偏移 10 字节
fseek(fp, -4, SEEK_END); // 从末尾向前偏移 4 字节(需确保位置合法)
文本流的偏移存在标准限制,不能普遍假定可按任意字节偏移;
ftell()在文本流中得到的值主要用作恢复位置的标记。
6. 二进制读写:fread() 与 fwrite()
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
返回值都是成功处理的完整元素个数,而不是字节数。
#include <stdio.h>
int main(void)
{
int numbers[] = {10, 20, 30, 40};
FILE *fp = fopen("numbers.bin", "wb");
if (fp == NULL) return 1;
size_t written = fwrite(numbers, sizeof(numbers[0]), 4, fp);
if (written != 4)
{
perror("fwrite");
fclose(fp);
return 1;
}
if (fclose(fp) == EOF) return 1;
int loaded[4];
fp = fopen("numbers.bin", "rb");
if (fp == NULL) return 1;
size_t read_count = fread(loaded, sizeof(loaded[0]), 4, fp);
if (read_count != 4)
{
fprintf(stderr, "Only read %zu elements\n", read_count);
fclose(fp);
return 1;
}
fclose(fp);
return 0;
}
提示:直接将 int 的内存表示写进文件,受 int 大小及大小端(Endianness)影响,不保证跨平台通用。稳定的文件格式应明确字节布局。
7. EOF、feof()、ferror()
不要写:
while (!feof(fp)) // 不推荐:在读取尝试到达末尾后才设置 EOF 标志
{
int c = fgetc(fp);
// 可能误处理 EOF
}
正确方式:根据读取函数的返回值控制循环,再检查错误。
int c;
while ((c = fgetc(fp)) != EOF)
{
putchar(c);
}
if (ferror(fp))
{
perror("Read error");
}
else if (feof(fp))
{
puts("Reached end of file");
}
8. 综合实例:复制任意二进制文件
#include <stdio.h>
int main(void)
{
FILE *src = fopen("input.bin", "rb");
if (src == NULL)
{
perror("input");
return 1;
}
FILE *dst = fopen("output.bin", "wb");
if (dst == NULL)
{
perror("output");
fclose(src);
return 1;
}
unsigned char buffer[4096];
size_t n;
int failed = 0;
while ((n = fread(buffer, 1, sizeof(buffer), src)) > 0)
{
if (fwrite(buffer, 1, n, dst) != n)
{
perror("write");
failed = 1;
break;
}
}
if (ferror(src))
{
perror("read");
failed = 1;
}
if (fclose(src) == EOF)
{
perror("close input");
failed = 1;
}
if (fclose(dst) == EOF)
{
perror("close output");
failed = 1;
}
return failed;
}
确保源文件与目标文件不是同一个文件,否则
"wb"打开目标文件时可能清空源数据。
9. 文件流与动态内存对比
| 比较 | malloc() |
fopen() |
|---|---|---|
| 返回 | 内存指针(如 int *) |
文件流指针 FILE * |
| 使用 | *p、p[i] |
fgetc、fprintf、fread 等 |
| 释放 | free(p) |
fclose(fp) |
| 失败时 | NULL |
NULL |
不能使用 free(fp) 代替 fclose(fp);fclose() 之后也不能继续使用该文件流。
10. 速查表
| 函数 | 用途 | 关键注意事项 |
|---|---|---|
fopen |
打开文件 | 检查 NULL |
fclose |
关闭文件 | 检查 EOF,写入错误也可能在关闭时暴露 |
fgetc |
读字符 | 使用 int 接收 |
fputc |
写字符 | 失败返回 EOF |
fgets |
读文本 | 有长度限制,可能保留换行 |
fputs |
写字符串 | 不自动换行 |
fscanf |
格式化读 | 检查成功赋值项数 |
fprintf |
格式化写 | 返回负值表示出错 |
fread |
二进制读 | 返回完整元素数 |
fwrite |
二进制写 | 返回完整元素数 |
fseek |
移动位置 | 文本流有限制 |
ftell |
查询位置 | 出错返回 -1L |
rewind |
回到开头 | 清除 EOF 和错误标志 |
feof |
测 EOF 状态 | 不作为读取循环的预先判断 |
ferror |
检查流错误 | 和 feof 区分 |
perror |
输出错误描述 | 用于错误诊断 |
remove |
删除文件 | 成功返回 0 |
rename |
重命名文件 | 成功返回 0 |
11. 常见错误与练习
常见错误: 忘记判断 fopen 结果;误用 "w" 导致数据被清空;读取 EOF 时使用 char;不检查读取结果;fclose 后继续操作流;忘记检查写入和关闭错误。
建议练习:
- 读取文本文件,统计字节数、单词数和行数。
- 将一个文本文件复制到另一个文本文件。
- 使用
fread/fwrite复制一张图片并对比源、目标文件。 - 将学生姓名与分数保存为 CSV 文件,再读取并求平均值。
核心:
FILE *管理的是文件流;读写函数操作流;EOF是读取返回状态,不是文件中的字符;成功打开的流应该正确关闭。