C标准库(libc)提供了一套带缓冲的文件操作函数,封装了底层的系统调用,提供了跨平台的文件读写能力。这套函数以 FILE*(文件流指针)为核心抽象,由库内部维护缓冲区,减少系统调用次数以提高效率。
C库IO与系统调用IO的关系:
用户程序
↓
C标准库(fopen/fread/fwrite/fclose) ← 带缓冲,跨平台
↓ 内部最终调用
系统调用(open/read/write/close) ← 无缓冲,Linux专属
↓
内核 VFS
↓
文件系统 / 设备驱动
为什么需要C库IO:
FILE结构体(核心抽象):
FILE 结构体是C库内部维护的一个结构,包含:
用户不需要关心FILE内部细节,只需要通过 FILE* 指针操作即可。
#include <stdio.h>
FILE *fopen(const char *pathname, const char *mode);
参数:
pathname:文件路径(相对路径或绝对路径)mode:打开模式字符串返回值:
FILE* 指针NULL,同时设置 errno打开模式详解:
| 模式 | 含义 | 文件不存在 | 文件已存在 | 从哪里开始写 |
|---|---|---|---|---|
"r" |
只读 | 出错 | 从头读 | — |
"r+" |
读写 | 出错 | 从头读写 | — |
"w" |
只写(截断) | 创建 | 清空 | 文件头 |
"w+" |
读写(截断) | 创建 | 清空 | 文件头 |
"a" |
追加写 | 创建 | 从末尾写 | 文件末尾 |
"a+" |
读+追加写 | 创建 | 读从头,写从末尾 | 文件末尾 |
带b的二进制模式: "rb" "wb" "ab" "rb+" 等,在Linux下没有区别(Linux不区分文本和二进制),但在Windows下会禁止 \n ↔ \r\n 转换。
易错点:
"w"模式会无条件清空文件内容,即使文件已有重要数据。想保留内容应使用"a"或"r+"。
#include <stdio.h>
int fclose(FILE *stream);
参数: stream:已打开的文件流指针
返回值: 成功返回 0,失败返回 EOF(通常是 -1)
重要: fclose 会先把缓冲区中未写入的数据刷新(flush)到文件,然后释放FILE结构体资源。如果不调用 fclose,缓冲区中的数据可能丢失。
易错点: 对同一个
FILE*多次调用fclose是未定义行为(double free)。
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数:
ptr:读取数据存放的缓冲区size:每个元素的字节大小nmemb:要读取的元素个数stream:文件流指针返回值: 成功返回实际读取的元素个数(不是字节数!),失败或到达文件末尾返回 0
总字节数 = size × 实际读取的元素个数
示例:读取一个int数组
int arr[10];
size_t n = fread(arr, sizeof(int), 10, fp);
// n 是实际读取的int个数,可能小于10(文件数据不足)
// 读取的字节数 = n * sizeof(int)
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参数:
ptr:要写入的数据缓冲区size:每个元素的字节大小nmemb:要写入的元素个数stream:文件流指针返回值: 成功返回实际写入的元素个数,失败返回 0
示例:写入一个字符串
const char *msg = "Hello, Linux!";
size_t len = strlen(msg);
size_t n = fwrite(msg, 1, len, fp);
// n 应该等于 len,否则写入不完整
#include <stdio.h>
int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
fprintf 类似 printf,但输出到文件而不是标准输出。
示例:写入结构化数据
struct Student {
char name[32];
int age;
float score;
};
struct Student s = {"Zhangsan", 20, 95.5};
fprintf(fp, "%s %d %.1f\n", s.name, s.age, s.score);
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
int fputs(const char *s, FILE *stream);
fgets 详解:
size - 1 个字符(留一个位置给 \0)\n(换行)停止读取,\n 会被读入缓冲区EOF 停止读取s(缓冲区指针),失败返回 NULL易错点:
fgets会把换行符\n也读进来,如果不需要换行,需要手动去除。
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
fseek 参数:
offset:偏移量(字节),可正可负whence:基准位置
SEEK_SET:文件开头(offset必须为正)SEEK_CUR:当前位置(offset可正可负)SEEK_END:文件末尾(offset通常为负)ftell: 返回当前文件位置(距文件开头的字节数),失败返回 -1L
rewind: 等价于 fseek(stream, 0, SEEK_SET) + 清除错误标志
示例:获取文件大小
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
rewind(fp); // 回到文件开头
printf("文件大小: %ld 字节\n", size);
#include <stdio.h>
int feof(FILE *stream); // 是否到达文件末尾
int ferror(FILE *stream); // 是否发生I/O错误
void clearerr(FILE *stream); // 清除错误标志
重要易错点:
feof只有在读取操作失败后才会返回非零值。不能在读取前用feof判断是否到文件末尾。正确做法是先读取,判断返回值,再用feof判断是否因为到达末尾而停止。
错误的EOF判断:
// 错误!feof在读取到末尾后下一次读取失败才返回真
while (!feof(fp)) {
fgets(buf, sizeof(buf), fp); // 最后一次循环会多读一次(重复最后一行)
process(buf);
}
正确的EOF判断:
// 正确:先读取,再判断返回值
while (fgets(buf, sizeof(buf), fp) != NULL) {
process(buf);
}
// 此时如果是因为到达EOF退出循环,feof(fp)为真
#include <stdio.h>
#include <stdlib.h>
#define BUF_SIZE 4096
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "用法: %s <源文件> <目标文件>\n", argv[0]);
return 1;
}
FILE *src = fopen(argv[1], "rb");
if (!src) {
perror("打开源文件失败");
return 1;
}
FILE *dst = fopen(argv[2], "wb");
if (!dst) {
perror("打开目标文件失败");
fclose(src);
return 1;
}
char buf[BUF_SIZE];
size_t n;
while ((n = fread(buf, 1, BUF_SIZE, src)) > 0) {
size_t written = fwrite(buf, 1, n, dst);
if (written != n) {
fprintf(stderr, "写入不完整\n");
fclose(src);
fclose(dst);
return 1;
}
}
fclose(src);
fclose(dst);
printf("复制完成\n");
return 0;
}
编译:gcc -o filecopy filecopy.c
#include <stdio.h>
#include <string.h>
typedef struct {
int id;
char name[32];
float score;
} Student;
void write_students(const char *filename, Student students[], int count) {
FILE *fp = fopen(filename, "wb");
if (!fp) {
perror("fopen");
return;
}
fwrite(&count, sizeof(int), 1, fp); // 先写入数量
fwrite(students, sizeof(Student), count, fp); // 再写入数据
fclose(fp);
}
int read_students(const char *filename, Student students[], int max_count) {
FILE *fp = fopen(filename, "rb");
if (!fp) {
perror("fopen");
return -1;
}
int count;
if (fread(&count, sizeof(int), 1, fp) != 1) {
fclose(fp);
return -1;
}
if (count > max_count) count = max_count;
size_t n = fread(students, sizeof(Student), count, fp);
fclose(fp);
return (int)n;
}
int main() {
Student students[] = {
{1, "Alice", 95.5},
{2, "Bob", 88.0},
{3, "Charlie", 92.3}
};
int count = sizeof(students) / sizeof(students[0]);
write_students("students.dat", students, count);
Student read_buf[10];
int n = read_students("students.dat", read_buf, 10);
printf("读取到 %d 条记录:\n", n);
for (int i = 0; i < n; i++) {
printf(" ID=%d, Name=%s, Score=%.1f\n",
read_buf[i].id, read_buf[i].name, read_buf[i].score);
}
return 0;
}
编译:gcc -o fileio_struct fileio_struct.c
#include <stdio.h>
#include <string.h>
#include <ctype.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "用法: %s <文件名>\n", argv[0]);
return 1;
}
FILE *fp = fopen(argv[1], "r");
if (!fp) {
perror("fopen");
return 1;
}
char line[1024];
int line_count = 0;
int word_count = 0;
int char_count = 0;
while (fgets(line, sizeof(line), fp) != NULL) {
line_count++;
char_count += strlen(line);
// 简单的单词计数:遇空白分隔
int in_word = 0;
for (int i = 0; line[i] != '\0'; i++) {
if (isspace(line[i])) {
in_word = 0;
} else if (!in_word) {
in_word = 1;
word_count++;
}
}
}
printf("行数: %d\n", line_count);
printf("单词数: %d\n", word_count);
printf("字符数: %d\n", char_count);
fclose(fp);
return 0;
}
编译:gcc -o wordcount wordcount.c
| 序号 | 坑点 | 说明 |
|---|---|---|
| 1 | fwrite返回值检查 | 返回值是元素个数,不是字节数;写入不完整时不会报错 |
| 2 | fgets会读入\n | 读入的字符串末尾包含换行符,需要手动去除 |
| 3 | feof不能先判断 | 必须先读取再判断feof,否则会多处理一行(重复最后一行) |
| 4 | fopen失败返回NULL | 必须检查返回值,不能直接使用FILE* |
| 5 | 缓冲区大小选择 | fread/fwrite的size参数影响缓冲效率,通常选择4KB的倍数 |
| 6 | 文本模式与二进制模式 | Linux下无区别,Windows下文本模式会做\n↔\r\n转换 |
| 7 | 文件指针位置 | fread/fwrite会自动移动文件指针,不需要手动fseek |
| 8 | fclose的flush作用 | 不调用fclose,缓冲区中的数据可能丢失 |
| 9 | fprintf返回值 | 返回写入的字符数,出错返回负数,可用于判断写入是否成功 |
| 10 | ftell在二进制文件中 | 二进制文件用ftell获取文件位置是可靠的;文本文件中换行符转换可能导致偏移不准确 |
Q: C库IO与系统调用IO的区别? A: C库IO(fopen等)在用户空间维护缓冲区,减少系统调用次数,效率更高;系统调用(open等)每次操作都陷入内核,开销大但更底层。C库IO提供格式化读写,系统调用只处理字节流。
Q: fread的返回值是什么含义? A: 返回的是成功读取的元素个数,不是字节数。如果文件剩余数据不足,实际读取的元素个数会小于请求的nmemb。总字节数 = size × 返回值。
Q: 为什么不能用while(!feof(fp))来循环读取?
A: feof 只有在读取操作失败后才会返回非零值。如果用 while(!feof(fp)),最后一次读取失败后feof才变为真,但此时循环体还会再执行一次,导致重复处理最后一行数据。
Q: 如何用C库函数获取文件大小?
A: fseek(fp, 0, SEEK_END) 移到末尾,ftell(fp) 返回文件大小(字节数),rewind(fp) 回到开头。注意二进制文件可靠,文本文件因换行转换可能不准确。
Q: fwrite写入不完整怎么处理? A: 检查fwrite返回值,如果不等于请求写入的元素个数,说明写入失败(磁盘满等)。需要处理错误并决定是否重试。