C语言文件 --- 完整知识点详解
一、文件概述
1.1 什么是文件
文件是指存储在外部介质(如硬盘、U盘)上的数据的集合。在C语言中,文件被看作是一个字节序列,即一个一个字节按顺序排列组成的数据流。
操作系统通过文件名来管理和访问文件。一个完整的文件名通常包括三部分:
文件名.扩展名
例如:data.txt、program.c、image.bmp
1.2 文件的分类
C语言中,文件按照数据的组织形式可以分为两大类:
(1)文本文件(ASCII文件)
- 文件中的每一个字节存储一个ASCII码,代表一个字符
- 例如:整数
10000在文本文件中占5个字节,分别存储字符'1'、'0'、'0'、'0'、'0'的ASCII码 - 特点:便于阅读,但占用空间较大,需要进行转换
(2)二进制文件
- 数据按照内存中的存储形式(二进制)直接存放到文件中
- 例如:整数
10000在二进制文件中占4个字节(int类型大小),存储其二进制形式00000000 00000000 00100111 00010000 - 特点:存储效率高,读写速度快,但不便于直接阅读
| 对比项 | 文本文件 | 二进制文件 |
|---|---|---|
| 存储方式 | 以ASCII字符形式存储 | 以二进制形式直接存储 |
| 可读性 | 可以用文本编辑器直接阅读 | 不可以直接阅读 |
| 存储效率 | 较低(需要转换) | 较高(直接存储) |
| 读写速度 | 较慢(需要编码转换) | 较快 |
| 典型应用 | 配置文件、日志文件、源代码 | 图片、音频、可执行文件 |
1.3 文件缓冲区
ANSI C标准采用 "缓冲文件系统" 来处理文件操作。其核心思想是:
-
系统在内存中为每个正在使用的文件开辟一块文件缓冲区
-
写文件时:程序先把数据输出到缓冲区,当缓冲区满了或文件关闭时,才将缓冲区中的数据一次性写入磁盘文件
-
读文件时:系统先从磁盘文件中读取一批数据到缓冲区,程序再从缓冲区中逐个读取数据
程序 ←→ 缓冲区 ←→ 磁盘文件
这样做的目的是减少磁盘读写次数,提高程序运行效率。
1.4 流(Stream)的概念
在C语言中,所有的I/O操作都是通过流来完成的。流是一个抽象的数据通道,程序通过流与外部设备(文件、键盘、显示器等)进行数据交换。
- 文本流:由一系列字符组成,用于处理文本文件
- 二进制流:由一系列字节组成,用于处理二进制文件
程序启动时,系统会自动打开三个标准流:
| 标准流 | 含义 | 默认设备 |
|---|---|---|
stdin |
标准输入流 | 键盘 |
stdout |
标准输出流 | 显示器 |
stderr |
标准错误流 | 显示器 |
二、文件基本操作
2.1 文件指针
文件指针 是C语言中进行文件操作的核心概念。每个被使用的文件,系统都在内存中为其开辟一个文件信息区 ,用来存放文件的相关信息(如文件名、文件状态、文件当前位置等)。这些信息保存在一个结构体类型的变量中,该结构体类型由系统定义,名为 FILE。
FILE结构体的大致内容:
c
// FILE结构体是系统定义的,以下为示意
typedef struct {
int _fd; // 文件描述符(文件号)
char *_buf; // 缓冲区指针
int _bufsiz; // 缓冲区大小
int _cnt; // 缓冲区中剩余的字符数
char *_ptr; // 当前读写位置指针
int _flag; // 文件状态标志
// ... 其他成员
} FILE;
注意 :我们不需要关心 FILE 结构体的具体实现细节,只需要使用
FILE *类型的指针即可。
声明文件指针的语法:
c
FILE *指针变量名;
示例:
c
FILE *fp; // 声明一个FILE类型的指针变量fp
为什么需要文件指针?
文件指针就像一把"钥匙",程序通过文件指针来找到并操作对应的文件。后续所有的文件读写操作,都需要通过文件指针来进行。
2.2 打开文件(fopen函数)
函数原型:
c
FILE *fopen(const char *filename, const char *mode);
参数说明:
filename:要打开的文件名(可以包含路径),字符串类型mode:文件打开模式,字符串类型
返回值:
- 成功:返回一个指向 FILE 结构体的指针
- 失败:返回
NULL
文件打开模式:
| 模式 | 含义 | 文件不存在时 | 文件存在时 | 要求文件 |
|---|---|---|---|---|
"r" |
只读(文本) | 出错返回NULL | 从头读取 | 必须存在 |
"w" |
只写(文本) | 创建新文件 | 清空原内容 | 不要求 |
"a" |
追加(文本) | 创建新文件 | 在末尾追加 | 不要求 |
"rb" |
只读(二进制) | 出错返回NULL | 从头读取 | 必须存在 |
"wb" |
只写(二进制) | 创建新文件 | 清空原内容 | 不要求 |
"ab" |
追加(二进制) | 创建新文件 | 在末尾追加 | 不要求 |
"r+" |
读写(文本) | 出错返回NULL | 从头读写 | 必须存在 |
"w+" |
读写(文本) | 创建新文件 | 清空原内容 | 不要求 |
"a+" |
读写(文本) | 创建新文件 | 可读,写在末尾 | 不要求 |
"rb+" / "r+b" |
读写(二进制) | 出错返回NULL | 从头读写 | 必须存在 |
"wb+" / "w+b" |
读写(二进制) | 创建新文件 | 清空原内容 | 不要求 |
"ab+" / "a+b" |
读写(二进制) | 创建新文件 | 可读,写在末尾 | 不要求 |
记忆规律:
r= read(读),w= write(写),a= append(追加)b= binary(二进制),不加b默认是文本模式+= 读写都允许
入门案例:
c
#include <stdio.h> // 包含标准输入输出头文件
#include <stdlib.h> // 包含exit函数所在头文件
int main()
{
FILE *fp; // 声明一个文件指针变量fp,类型为FILE*
// 以只读方式打开当前目录下的data.txt文件
fp = fopen("data.txt", "r");
// 判断文件是否打开成功
// fopen打开失败时返回NULL,所以必须进行判断
if (fp == NULL)
{
printf("文件打开失败!\n"); // 输出错误提示信息
exit(1); // 终止程序,exit需要stdlib.h
}
printf("文件打开成功!\n"); // 打开成功,输出提示
fclose(fp); // 关闭文件,释放资源
return 0; // 程序正常结束
}
带路径的文件打开方式:
c
// 打开D盘test目录下的文件
fp = fopen("D:\\test\\data.txt", "r");
// 注意:路径中的反斜杠要用双反斜杠 \\ 进行转义
// 因为单个 \ 在C语言中是转义字符
2.3 关闭文件(fclose函数)
函数原型:
c
int fclose(FILE *stream);
参数说明:
stream:指向要关闭的文件的FILE指针
返回值:
- 成功:返回
0 - 失败:返回
EOF(EOF通常定义为-1)
为什么要关闭文件?
- 刷新缓冲区:将缓冲区中尚未写入文件的数据写入磁盘,防止数据丢失
- 释放资源:释放文件信息区(FILE结构体)占用的内存
- 释放文件号:操作系统能同时打开的文件数量有限,及时关闭可以释放文件号
重要原则:
- 打开的文件必须在使用完之后关闭
- 一个 FILE 指针关闭后,不再指向有效文件,不应当再使用
- 可以将指针置为NULL,避免误操作
c
fclose(fp); // 关闭文件
fp = NULL; // 将指针置为NULL,安全起见
入门案例:完整的文件打开与关闭
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp; // 声明文件指针
// 以写入模式打开文件test.txt
// 如果文件不存在会创建新文件,如果存在会清空内容
fp = fopen("test.txt", "w");
// 判断文件是否成功打开
if (fp == NULL)
{
printf("无法打开文件!\n"); // 提示错误
exit(1); // 退出程序
}
printf("文件已成功打开,准备写入数据...\n");
// 在这里可以进行文件读写操作...
// 关闭文件
if (fclose(fp) == 0)
{
printf("文件已成功关闭。\n"); // fclose返回0表示成功
}
else
{
printf("文件关闭失败!\n"); // fclose返回EOF表示失败
}
return 0;
}
三、文件的读写
3.1 写入字符(fputc函数)
函数原型:
c
int fputc(int ch, FILE *stream);
参数说明:
ch:要写入的字符(以int形式传递,实际使用char即可)stream:目标文件的FILE指针
返回值:
- 成功:返回写入的字符(以int形式)
- 失败:返回
EOF(-1)
功能:将一个字符写入到指定的文件流中。
入门案例:向文件写入单个字符
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp; // 声明文件指针
char ch; // 声明字符变量,用于存储要写入的字符
// 以写入模式打开文件output.txt
// "w"模式:文件不存在则创建,存在则清空
fp = fopen("output.txt", "w");
// 判断文件是否打开成功
if (fp == NULL)
{
printf("文件打开失败!\n"); // 提示错误信息
exit(1); // 退出程序
}
// 使用循环将字符'A'到'Z'依次写入文件
for (ch = 'A'; ch <= 'Z'; ch++)
{
// fputc每次写入一个字符到文件fp中
fputc(ch, fp);
}
// 写入一个换行符
fputc('\n', fp);
printf("字符写入完成!\n"); // 提示写入完成
fclose(fp); // 关闭文件
return 0;
}
运行后,output.txt 文件中的内容为:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
3.2 读取字符(fgetc函数)
函数原型:
c
int fgetc(FILE *stream);
参数说明:
stream:源文件的FILE指针
返回值:
- 成功:返回读取到的字符(以int形式表示)
- 读到文件末尾或出错:返回
EOF(-1)
功能:从文件流中读取一个字符,并使文件位置指针向后移动一个位置。
重要说明:
fgetc每次调用只读取一个字符- 每读取一个字符,文件内部的位置指针自动后移一位
- 当读到文件末尾时,返回
EOF EOF既表示文件结束,也可能表示读取出错,可以用feof()和ferror()来区分
入门案例:从文件逐字符读取内容
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp; // 声明文件指针
char ch; // 声明字符变量,存储读取到的字符
// 以只读模式打开前面创建的output.txt文件
fp = fopen("output.txt", "r");
// 判断文件是否打开成功
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
printf("文件内容为:\n");
// 循环读取文件中的每一个字符
// fgetc返回EOF表示到达文件末尾(或出错)
while ((ch = fgetc(fp)) != EOF)
{
putchar(ch); // 将读取到的字符输出到屏幕
}
printf("\n文件读取完成!\n");
fclose(fp); // 关闭文件
return 0;
}
运行结果:
文件内容为:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
文件读取完成!
入门案例:fputc和fgetc配合使用------逐字符复制文件
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp_src; // 源文件指针
FILE *fp_dst; // 目标文件指针
char ch; // 用于中转的字符变量
// 以只读模式打开源文件
fp_src = fopen("output.txt", "r");
if (fp_src == NULL)
{
printf("源文件打开失败!\n");
exit(1);
}
// 以写入模式打开(创建)目标文件
fp_dst = fopen("copy.txt", "w");
if (fp_dst == NULL)
{
printf("目标文件打开失败!\n");
fclose(fp_src); // 注意:关闭已打开的源文件,避免资源泄漏
exit(1);
}
// 循环从源文件读取一个字符,写入目标文件
// 直到源文件读取完毕(fgetc返回EOF)
while ((ch = fgetc(fp_src)) != EOF)
{
fputc(ch, fp_dst); // 将字符写入目标文件
}
printf("文件复制完成!\n");
// 关闭两个文件
fclose(fp_src);
fclose(fp_dst);
return 0;
}
3.3 写入字符串(fputs函数)
函数原型:
c
int fputs(const char *str, FILE *stream);
参数说明:
str:要写入的字符串(以'\0'结尾的字符数组或字符串常量)stream:目标文件的FILE指针
返回值:
- 成功:返回一个非负整数(通常为0)
- 失败:返回
EOF
功能:将一个字符串写入到指定文件流中。
重要说明:
fputs会将字符串中的所有字符写入文件,但不包括结尾的'\0'fputs不会自动在末尾添加换行符 ,需要手动添加'\n'
入门案例:向文件写入多行字符串
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp; // 声明文件指针
// 以写入模式打开文件
fp = fopen("student.txt", "w");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
// 使用fputs写入字符串到文件
// 注意:fputs不会自动换行,需要手动添加\n
fputs("张三\n", fp); // 写入姓名后换行
fputs("男\n", fp); // 写入性别后换行
fputs("20岁\n", fp); // 写入年龄后换行
fputs("计算机科学\n", fp); // 写入专业后换行
printf("数据写入完成!\n");
fclose(fp); // 关闭文件
return 0;
}
运行后,student.txt 文件中的内容为:
张三
男
20岁
计算机科学
3.4 读取字符串(fgets函数)
函数原型:
c
char *fgets(char *str, int n, FILE *stream);
参数说明:
str:存放读取字符串的字符数组(缓冲区)n:最多读取的字符数(包括'\0'),即最多读取n-1个有效字符stream:源文件的FILE指针
返回值:
- 成功:返回
str的首地址(即传入的数组地址) - 失败或读到文件末尾:返回
NULL
功能:从文件流中读取一行字符串。
重要说明:
fgets最多读取n-1个字符,然后自动加上'\0'- 如果在读满
n-1个字符之前遇到换行符'\n',则读取结束,并且'\n'也会被读入并存储在字符串中 - 如果读到文件末尾,也会结束读取
- 区别于
gets函数,fgets更安全,可以限制读取长度
入门案例:从文件读取字符串
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // 包含字符串处理函数
int main()
{
FILE *fp; // 声明文件指针
char buffer[100]; // 声明字符数组作为缓冲区,最多存储100个字符
// 以只读模式打开前面创建的student.txt文件
fp = fopen("student.txt", "r");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
printf("文件内容:\n");
// 循环逐行读取文件内容
// fgets每次读取一行,最多读取99个字符(留1个位置给'\0')
while (fgets(buffer, 100, fp) != NULL)
{
printf("%s", buffer); // 输出读取到的字符串
// 注意:fgets会保留行末的\n,所以不需要额外加\n
}
printf("\n文件读取完成!\n");
fclose(fp); // 关闭文件
return 0;
}
运行结果:
文件内容:
张三
男
20岁
计算机科学
文件读取完成!
3.5 fprintf函数
函数原型:
c
int fprintf(FILE *stream, const char *format, ...);
参数说明:
stream:目标文件的FILE指针format:格式控制字符串(与printf的格式字符串完全相同)...:可变参数列表
返回值:
- 成功:返回实际写入的字符数
- 失败:返回一个负数
功能:按照格式控制字符串,将数据格式化写入文件。
fprintf 与 printf 的关系:
printf(...)等价于fprintf(stdout, ...)fprintf就是可以指定输出目标(文件)的printf
常用格式说明符:
| 格式符 | 含义 | 示例 |
|---|---|---|
%d |
十进制整数 | fprintf(fp, "%d", 100) |
%f |
浮点数 | fprintf(fp, "%f", 3.14) |
%c |
单个字符 | fprintf(fp, "%c", 'A') |
%s |
字符串 | fprintf(fp, "%s", "hello") |
%lf |
双精度浮点数 | fprintf(fp, "%lf", 3.14) |
%5d |
宽度为5的整数 | 右对齐,左边补空格 |
%.2f |
保留2位小数 | fprintf(fp, "%.2f", 3.1415) → 3.14 |
入门案例:格式化写入学生信息到文件
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp; // 声明文件指针
// 声明并初始化学生信息变量
char name[20] = "李四"; // 姓名
int age = 22; // 年龄
float score = 95.5; // 成绩
char gender = 'M'; // 性别
// 以写入模式打开文件info.txt
fp = fopen("info.txt", "w");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
// 使用fprintf将格式化数据写入文件
fprintf(fp, "姓名:%s\n", name); // 写入姓名
fprintf(fp, "年龄:%d岁\n", age); // 写入年龄
fprintf(fp, "成绩:%.1f分\n", score); // 写入成绩,保留1位小数
fprintf(fp, "性别:%c\n", gender); // 写入性别
// 也可以一行写入多个数据(如表格数据)
fprintf(fp, "%s %d %.1f %c\n", name, age, score, gender);
printf("格式化数据写入完成!\n");
fclose(fp); // 关闭文件
return 0;
}
运行后,info.txt 文件中的内容为:
姓名:李四
年龄:22岁
成绩:95.5分
性别:M
李四 22 95.5 M
3.6 fscanf函数
函数原型:
c
int fscanf(FILE *stream, const char *format, ...);
参数说明:
stream:源文件的FILE指针format:格式控制字符串(与scanf的格式字符串完全相同)...:接收数据的变量地址
返回值:
- 成功:返回成功读取并赋值的数据项个数
- 遇到文件末尾:返回
EOF
功能:按照格式控制字符串,从文件中读取格式化数据。
fscanf 与 scanf 的关系:
scanf(...)等价于fscanf(stdin, ...)fscanf就是可以指定数据来源(文件)的scanf
入门案例:从文件中格式化读取学生信息
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp; // 声明文件指针
// 声明变量用于存储读取的数据
char name[20]; // 姓名
int age; // 年龄
float score; // 成绩
char gender; // 性别
// 以只读模式打开文件info.txt
fp = fopen("info.txt", "r");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
// 读取第一行"姓名:xxx\n" ------用fscanf按格式读取
fscanf(fp, "姓名:%s\n", name); // 跳过"姓名:"前缀,读取字符串
fscanf(fp, "年龄:%d岁\n", &age); // 跳过"年龄:"前缀,读取整数
fscanf(fp, "成绩:%f分\n", &score); // 跳过"成绩:"前缀,读取浮点数
fscanf(fp, "性别:%c\n", &gender); // 跳过"性别:"前缀,读取字符
// 输出读取到的数据
printf("读取到的学生信息:\n");
printf("姓名:%s\n", name);
printf("年龄:%d岁\n", age);
printf("成绩:%.1f分\n", score);
printf("性别:%c\n", gender);
fclose(fp); // 关闭文件
return 0;
}
运行结果:
读取到的学生信息:
姓名:李四
年龄:22岁
成绩:95.5分
性别:M
fprintf和fscanf配合使用案例:录入和显示多个学生信息
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp; // 声明文件指针
int i; // 循环变量
// ========== 写入阶段 ==========
// 以写入模式打开文件score.txt
fp = fopen("score.txt", "w");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
printf("请输入3个学生的姓名和成绩:\n");
// 循环输入3个学生的信息并写入文件
for (i = 0; i < 3; i++)
{
char name[20]; // 临时存储姓名
float score; // 临时存储成绩
printf("第%d个学生(姓名 成绩):", i + 1);
scanf("%s %f", name, &score); // 从键盘读取
// 将数据格式化写入文件,以空格分隔
fprintf(fp, "%s %.1f\n", name, score);
}
fclose(fp); // 写入完毕,关闭文件
// ========== 读取阶段 ==========
// 以只读模式重新打开文件
fp = fopen("score.txt", "r");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
printf("\n从文件中读取的学生信息:\n");
printf("%-10s %s\n", "姓名", "成绩"); // 打印表头
printf("-------------------\n");
char name[20]; // 存储读取的姓名
float score; // 存储读取的成绩
// 使用fscanf从文件中循环读取数据
// fscanf返回成功读取的数据项数,2表示成功读取了2项
while (fscanf(fp, "%s %f", name, &score) == 2)
{
printf("%-10s %.1f\n", name, score); // 左对齐输出
}
fclose(fp); // 读取完毕,关闭文件
return 0;
}
运行示例:
请输入3个学生的姓名和成绩:
第1个学生(姓名 成绩):Alice 90.5
第2个学生(姓名 成绩):Bob 85.0
第3个学生(姓名 成绩):Charlie 92.3
从文件中读取的学生信息:
姓名 成绩
-------------------
Alice 90.5
Bob 85.0
Charlie 92.3
3.7 读写数据块(fread和fwrite函数)
前面介绍的函数适合处理文本数据 。当我们需要高效地读写大量结构化数据 (如结构体数组)时,使用 fread 和 fwrite 以二进制方式读写数据块更加高效。
fwrite函数 ------ 写数据块
函数原型:
c
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
参数说明:
ptr:指向要写入数据的内存起始地址(通常传数组名或结构体指针)size:每个数据块的字节大小(通常用sizeof运算符获取)count:要写入的数据块个数stream:目标文件的FILE指针
返回值:
- 成功:返回实际写入的数据块个数(不是字节数)
- 如果返回值小于
count,说明写入过程中出现了错误
功能 :将内存中 count 个大小为 size 字节的数据块写入文件。
fread函数 ------ 读数据块
函数原型:
c
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
参数说明:
ptr:指向存储读取数据的内存起始地址size:每个数据块的字节大小count:要读取的数据块个数stream:源文件的FILE指针
返回值:
- 成功:返回实际读取的数据块个数
- 如果返回值小于
count,可能是遇到了文件末尾或发生了错误
入门案例:使用fwrite和fread读写结构体数组
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义学生结构体
struct Student
{
char name[20]; // 姓名
int age; // 年龄
float score; // 成绩
};
int main()
{
FILE *fp; // 声明文件指针
int i; // 循环变量
// ========== 初始化学生数据 ==========
struct Student stu_write[3] = // 要写入文件的学生数组
{
{"张三", 20, 88.5},
{"李四", 21, 92.0},
{"王五", 19, 95.5}
};
struct Student stu_read[3]; // 用于存储从文件读取的数据
// ========== 写入阶段 ==========
// 以二进制写入模式打开文件student.dat
// "wb"表示写入二进制文件
fp = fopen("student.dat", "wb");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
// 使用fwrite将整个数组写入文件
// 参数1:数据的起始地址(数组名)
// 参数2:每个元素的大小
// 参数3:元素个数
// 参数4:文件指针
size_t written = fwrite(stu_write, sizeof(struct Student), 3, fp);
printf("成功写入 %zu 个学生数据。\n", written);
fclose(fp); // 关闭文件
// ========== 读取阶段 ==========
// 以二进制只读模式打开文件
fp = fopen("student.dat", "rb");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
// 使用fread从文件读取3个学生数据到数组
fread(stu_read, sizeof(struct Student), 3, fp);
printf("\n从文件读取的学生信息:\n");
printf("%-10s %-6s %s\n", "姓名", "年龄", "成绩");
printf("----------------------------\n");
// 循环输出读取到的每个学生信息
for (i = 0; i < 3; i++)
{
printf("%-10s %-6d %.1f\n",
stu_read[i].name,
stu_read[i].age,
stu_read[i].score);
}
fclose(fp); // 关闭文件
return 0;
}
运行结果:
成功写入 3 个学生数据。
从文件读取的学生信息:
姓名 年龄 成绩
----------------------------
张三 20 88.5
李四 21 92.0
王五 19 95.5
四、文件的定位
在每个打开的文件中,都有一个文件位置指示器 (也叫文件指针位置或文件读写位置),用来指示当前读写操作的位置。每次读写操作后,位置指示器会自动向后移动。
C语言提供以下函数来人为控制文件位置指示器的位置。
4.1 fseek函数
函数原型:
c
int fseek(FILE *stream, long offset, int origin);
参数说明:
stream:文件指针offset:相对于基准位置的偏移量(字节数)- 正数:向文件末尾方向移动
- 负数:向文件开头方向移动
- 零:不移动
origin:基准位置(起始点),有三个可选值:
| 常量 | 值 | 含义 |
|---|---|---|
SEEK_SET |
0 | 文件开头 |
SEEK_CUR |
1 | 当前位置 |
SEEK_END |
2 | 文件末尾 |
返回值:
- 成功:返回
0 - 失败:返回非零值
功能:将文件位置指示器移动到指定位置。
常见用法:
c
fseek(fp, 0, SEEK_SET); // 将位置移到文件开头
fseek(fp, 0, SEEK_END); // 将位置移到文件末尾
fseek(fp, 100, SEEK_SET); // 从文件开头向后移动100个字节
fseek(fp, -10, SEEK_CUR); // 从当前位置向前移动10个字节
入门案例:使用fseek随机访问文件中的学生数据
c
#include <stdio.h>
#include <stdlib.h>
struct Student
{
char name[20];
int age;
float score;
};
int main()
{
FILE *fp;
struct Student stu; // 用于存储读取的单个学生数据
// 以二进制读模式打开文件(使用前面创建的student.dat)
fp = fopen("student.dat", "rb");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
// ===== 读取第3个学生(索引为2)的数据 =====
// 先用fseek定位到第3个学生的起始位置
// 偏移量 = 2 * sizeof(struct Student)
// 从文件开头(SEEK_SET)偏移2个结构体大小的位置
fseek(fp, 2 * sizeof(struct Student), SEEK_SET);
// 从当前位置读取1个学生数据
fread(&stu, sizeof(struct Student), 1, fp);
printf("第3个学生的信息:\n");
printf("姓名:%s\n", stu.name);
printf("年龄:%d\n", stu.age);
printf("成绩:%.1f\n", stu.score);
// ===== 读取第1个学生(索引为0)的数据 =====
fseek(fp, 0 * sizeof(struct Student), SEEK_SET); // 定位到文件开头
fread(&stu, sizeof(struct Student), 1, fp);
printf("\n第1个学生的信息:\n");
printf("姓名:%s\n", stu.name);
printf("年龄:%d\n", stu.age);
printf("成绩:%.1f\n", stu.score);
fclose(fp); // 关闭文件
return 0;
}
运行结果:
第3个学生的信息:
姓名:王五
年龄:19
成绩:95.5
第1个学生的信息:
姓名:张三
年龄:20
成绩:88.5
4.2 rewind函数
函数原型:
c
void rewind(FILE *stream);
参数说明:
stream:文件指针
返回值:无
功能 :将文件位置指示器重置到文件开头,同时清除文件的错误标志。
等价于:
c
fseek(fp, 0, SEEK_SET);
clearerr(fp);
入门案例:使用rewind多次读取文件
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
char ch;
// 以只读模式打开文件
fp = fopen("output.txt", "r");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
// ===== 第一次读取:逐字符输出 =====
printf("第一次读取:\n");
while ((ch = fgetc(fp)) != EOF)
{
putchar(ch); // 输出字符
}
printf("\n");
// ===== 使用rewind将文件指针重置到开头 =====
rewind(fp);
// ===== 第二次读取:逐字符输出 =====
printf("第二次读取:\n");
while ((ch = fgetc(fp)) != EOF)
{
putchar(ch);
}
printf("\n");
fclose(fp); // 关闭文件
return 0;
}
运行结果:
第一次读取:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
第二次读取:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
4.3 ftell函数
函数原型:
c
long ftell(FILE *stream);
参数说明:
stream:文件指针
返回值:
- 成功:返回当前文件位置指示器相对于文件开头的偏移量(字节数)
- 失败:返回
-1L
功能:获取文件位置指示器的当前位置。
入门案例:使用ftell获取文件大小
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
long file_size; // 存储文件大小
// 以只读模式打开文件
fp = fopen("output.txt", "r");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
// 先将文件指针移到文件末尾
fseek(fp, 0, SEEK_END);
// 使用ftell获取当前位置(即文件大小,单位为字节)
file_size = ftell(fp);
printf("文件大小为:%ld 字节\n", file_size);
fclose(fp); // 关闭文件
return 0;
}
运行结果(output.txt 包含26个英文字母):
文件大小为:26 字节
综合案例:fseek、rewind和ftell配合使用
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
long pos; // 存储文件位置
char ch; // 存储读取的字符
fp = fopen("output.txt", "r");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
// 读取前3个字符
ch = fgetc(fp); putchar(ch); // 读取'A'
ch = fgetc(fp); putchar(ch); // 读取'B'
ch = fgetc(fp); putchar(ch); // 读取'C'
// 获取当前文件位置
pos = ftell(fp);
printf("\n当前文件位置:%ld\n", pos); // 输出3(已读3个字节)
// 向前回退1个字节(回到第3个字符的位置)
fseek(fp, -1, SEEK_CUR);
pos = ftell(fp);
printf("回退后文件位置:%ld\n", pos); // 输出2
// 再次读取一个字符
ch = fgetc(fp);
printf("当前位置的字符:%c\n", ch); // 输出'C'(位置2对应第3个字符)
// 重置到文件开头
rewind(fp);
pos = ftell(fp);
printf("rewind后文件位置:%ld\n", pos); // 输出0
fclose(fp);
return 0;
}
运行结果:
ABC
当前文件位置:3
回退后文件位置:2
当前位置的字符:C
rewind后文件位置:0
五、文件管理
5.1 删除文件(remove函数)
函数原型:
c
int remove(const char *filename);
参数说明:
filename:要删除的文件名(可以包含路径)
返回值:
- 成功:返回
0 - 失败:返回
-1
功能:删除指定的文件。
入门案例:删除文件
c
#include <stdio.h>
int main()
{
char filename[100]; // 存储文件名
printf("请输入要删除的文件名:");
scanf("%s", filename); // 从键盘读取文件名
// 使用remove函数删除文件
if (remove(filename) == 0)
{
printf("文件 \"%s\" 删除成功!\n", filename);
}
else
{
printf("文件 \"%s\" 删除失败!(文件可能不存在)\n", filename);
}
return 0;
}
运行示例:
请输入要删除的文件名:copy.txt
文件 "copy.txt" 删除成功!
5.2 重命名文件(rename函数)
函数原型:
c
int rename(const char *oldname, const char *newname);
参数说明:
oldname:原来的文件名newname:新的文件名
返回值:
- 成功:返回
0 - 失败:返回
-1
功能 :将文件从 oldname 重命名为 newname。也可以用来移动文件(如果新旧文件名包含不同路径)。
入门案例:重命名文件
c
#include <stdio.h>
int main()
{
char old_name[100]; // 存储原文件名
char new_name[100]; // 存储新文件名
printf("请输入原文件名:");
scanf("%s", old_name);
printf("请输入新文件名:");
scanf("%s", new_name);
// 使用rename函数重命名文件
if (rename(old_name, new_name) == 0)
{
printf("文件重命名成功!\n");
printf("\"%s\" -> \"%s\"\n", old_name, new_name);
}
else
{
printf("文件重命名失败!\n");
}
return 0;
}
运行示例:
请输入原文件名:info.txt
请输入新文件名:student_info.txt
文件重命名成功!
"info.txt" -> "student_info.txt"
5.3 复制文件
C语言标准库没有提供直接的复制文件函数,需要手动实现。实现思路是:打开源文件读取内容,同时打开目标文件写入内容。
入门案例:逐字节复制文件
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp_src; // 源文件指针
FILE *fp_dst; // 目标文件指针
char src_name[100]; // 源文件名
char dst_name[100]; // 目标文件名
char ch; // 用于中转的字符变量
// 输入源文件名和目标文件名
printf("请输入源文件名:");
scanf("%s", src_name);
printf("请输入目标文件名:");
scanf("%s", dst_name);
// 以二进制读模式打开源文件
fp_src = fopen(src_name, "rb");
if (fp_src == NULL)
{
printf("源文件 \"%s\" 打开失败!\n", src_name);
exit(1);
}
// 以二进制写模式打开(创建)目标文件
fp_dst = fopen(dst_name, "wb");
if (fp_dst == NULL)
{
printf("目标文件 \"%s\" 打开失败!\n", dst_name);
fclose(fp_src); // 关闭已打开的源文件
exit(1);
}
// 逐字节复制:从源文件读一个字节,写入目标文件
while ((ch = fgetc(fp_src)) != EOF)
{
fputc(ch, fp_dst);
}
printf("文件复制完成!\n");
printf("\"%s\" -> \"%s\"\n", src_name, dst_name);
// 关闭两个文件
fclose(fp_src);
fclose(fp_dst);
return 0;
}
六、文件状态检测
6.1 文件读取结束检测(feof函数)
函数原型:
c
int feof(FILE *stream);
参数说明:
stream:文件指针
返回值:
- 如果文件位置指示器已经到达文件末尾 :返回非零值(真)
- 如果文件位置指示器未到达文件末尾 :返回
0(假)
功能:检测文件是否读取到末尾。
重要说明:
feof是在上一次读操作之后检测是否已经到达文件末尾- 不能在读取之前调用
feof来预判是否到达末尾 - 正确的使用方式:先读取,再判断
常见的使用模式:
c
// 方式一:先读后判断(推荐)
while (1)
{
ch = fgetc(fp); // 先读取一个字符
if (feof(fp)) break; // 判断是否到达文件末尾
putchar(ch); // 处理读取到的字符
}
// 方式二:利用fgetc的返回值判断(前面章节使用的方式)
while ((ch = fgetc(fp)) != EOF)
{
putchar(ch);
}
入门案例:使用feof判断文件读取结束
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
char buffer[50]; // 缓冲区
// 以写入模式创建测试文件
fp = fopen("feof_test.txt", "w");
if (fp == NULL)
{
printf("文件创建失败!\n");
exit(1);
}
// 写入测试数据
fputs("Hello\n", fp);
fputs("World\n", fp);
fputs("C Language\n", fp);
fclose(fp); // 关闭写入文件
// 以只读模式重新打开文件
fp = fopen("feof_test.txt", "r");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
printf("文件内容:\n");
// 使用feof检测文件末尾
// 先fgets读取一行,再用feof判断是否到达末尾
while (1)
{
// 读取一行到缓冲区
if (fgets(buffer, 50, fp) == NULL)
{
break; // 读取失败则退出
}
printf("%s", buffer); // 输出该行
// 检测是否到达文件末尾
if (feof(fp))
{
printf("(已到达文件末尾)\n");
break;
}
}
fclose(fp); // 关闭文件
return 0;
}
运行结果:
文件内容:
Hello
World
C Language
(已到达文件末尾)
6.2 文件读写出错检测(ferror函数)
函数原型:
c
int ferror(FILE *stream);
参数说明:
stream:文件指针
返回值:
- 如果文件操作过程中发生了错误 :返回非零值(真)
- 如果未发生错误 :返回
0(假)
功能:检测文件流中是否发生了读写错误。
常见的错误原因:
- 磁盘已满
- 没有写权限
- 磁盘硬件故障
- 试图向只读文件写入
入门案例:检测文件读写错误
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
// 尝试以写入模式打开一个位于受保护路径的文件
// 这里我们故意尝试写入一个可能出错的情况
fp = fopen("test_error.txt", "w");
if (fp == NULL)
{
printf("文件打开失败!\n");
exit(1);
}
// 尝试写入数据
fputs("Testing ferror function.\n", fp);
// 使用ferror检测是否发生错误
if (ferror(fp))
{
printf("文件写入过程中发生错误!\n");
clearerr(fp); // 清除错误标志
}
else
{
printf("文件写入正常,未发生错误。\n");
}
fclose(fp);
return 0;
}
运行结果:
文件写入正常,未发生错误。
6.3 清除状态标志(clearerr函数)
函数原型:
c
void clearerr(FILE *stream);
参数说明:
stream:文件指针
返回值:无
功能 :清除文件流的错误标志 和文件结束标志。
为什么需要clearerr?
- 当文件操作出现错误(
ferror返回非零)或到达文件末尾(feof返回非零)后,这些标志位会一直保持 - 如果想继续操作同一个文件流(如重新定位后再读取),必须先用
clearerr清除这些标志 - 否则后续操作可能受到影响
入门案例:使用clearerr清除文件结束标志后重新读取
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
char ch;
// 创建测试文件并写入数据
fp = fopen("clearerr_test.txt", "w");
if (fp == NULL) { printf("创建失败!\n"); exit(1); }
fputs("ABCDE", fp);
fclose(fp);
// 以只读模式打开文件
fp = fopen("clearerr_test.txt", "r");
if (fp == NULL) { printf("打开失败!\n"); exit(1); }
// 第一次读取:读到文件末尾
printf("第一次读取:");
while ((ch = fgetc(fp)) != EOF)
{
putchar(ch);
}
printf("\n");
// 此时feof(fp)应该返回非零值(文件结束标志被设置)
printf("feof状态:%d(非0表示已到末尾)\n", feof(fp));
// ===== 使用clearerr清除文件结束标志 =====
clearerr(fp);
printf("清除后feof状态:%d(0表示标志已清除)\n", feof(fp));
// 使用rewind将文件指针移回开头
rewind(fp);
// 第二次读取:可以正常读取
printf("第二次读取:");
while ((ch = fgetc(fp)) != EOF)
{
putchar(ch);
}
printf("\n");
fclose(fp);
return 0;
}
运行结果:
第一次读取:ABCDE
feof状态:32(非0表示已到末尾)
清除后feof状态:0(0表示标志已清除)
第二次读取:ABCDE
七、综合实践案例
案例一:简单的通讯录系统(文本文件版)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义联系人结构体
struct Contact
{
char name[50]; // 姓名
char phone[20]; // 电话号码
};
// 函数声明
void addContact(FILE *fp); // 添加联系人
void showAllContacts(FILE *fp); // 显示所有联系人
int main()
{
FILE *fp; // 文件指针
int choice; // 用户选择
// 主循环:显示菜单,接收用户选择
while (1)
{
// 显示操作菜单
printf("\n===== 通讯录管理系统 =====\n");
printf("1. 添加联系人\n");
printf("2. 显示所有联系人\n");
printf("3. 退出\n");
printf("请输入选择:");
scanf("%d", &choice);
switch (choice)
{
case 1:
// 以追加模式打开文件(在末尾添加,不覆盖原有内容)
fp = fopen("contacts.txt", "a");
if (fp == NULL)
{
printf("文件打开失败!\n");
break;
}
addContact(fp); // 调用添加联系人函数
fclose(fp); // 关闭文件
break;
case 2:
// 以只读模式打开文件
fp = fopen("contacts.txt", "r");
if (fp == NULL)
{
printf("通讯录为空或文件打开失败!\n");
break;
}
showAllContacts(fp); // 调用显示所有联系人函数
fclose(fp); // 关闭文件
break;
case 3:
printf("感谢使用,再见!\n");
exit(0); // 退出程序
default:
printf("无效选择,请重新输入!\n");
}
}
return 0;
}
// 添加联系人函数
void addContact(FILE *fp)
{
struct Contact c; // 声明一个联系人变量
printf("请输入姓名:");
scanf("%s", c.name);
printf("请输入电话号码:");
scanf("%s", c.phone);
// 将联系人信息写入文件,格式为:姓名 电话号码
fprintf(fp, "%s %s\n", c.name, c.phone);
printf("联系人添加成功!\n");
}
// 显示所有联系人函数
void showAllContacts(FILE *fp)
{
struct Contact c; // 用于临时存储读取的联系人信息
printf("\n%-15s %s\n", "姓名", "电话号码");
printf("------------------------------\n");
// 循环从文件读取联系人信息
while (fscanf(fp, "%s %s", c.name, c.phone) == 2)
{
printf("%-15s %s\n", c.name, c.phone);
}
}
运行示例:
===== 通讯录管理系统 =====
1. 添加联系人
2. 显示所有联系人
3. 退出
请输入选择:1
请输入姓名:张三
请输入电话号码:13800001111
联系人添加成功!
===== 通讯录管理系统 =====
1. 添加联系人
2. 显示所有联系人
3. 退出
请输入选择:2
姓名 电话号码
------------------------------
张三 13800001111
案例二:学生管理系统(二进制文件版)
c
#include <stdio.h>
#include <stdlib.h>
// 定义学生结构体
struct Student
{
int id; // 学号
char name[20]; // 姓名
float score; // 成绩
};
// 函数声明
void addStudent(); // 添加学生信息
void displayAll(); // 显示所有学生信息
void searchById(); // 按学号查找学生
void modifyScore(); // 修改学生成绩
int main()
{
int choice; // 用户菜单选择
while (1)
{
// 显示主菜单
printf("\n===== 学生信息管理系统 =====\n");
printf("1. 添加学生信息\n");
printf("2. 显示所有学生信息\n");
printf("3. 按学号查找学生\n");
printf("4. 修改学生成绩\n");
printf("5. 退出\n");
printf("请输入选择:");
scanf("%d", &choice);
switch (choice)
{
case 1: addStudent(); break; // 添加
case 2: displayAll(); break; // 显示所有
case 3: searchById(); break; // 查找
case 4: modifyScore(); break; // 修改
case 5:
printf("程序结束!\n");
exit(0);
default:
printf("无效选择!\n");
}
}
return 0;
}
// 添加学生信息
void addStudent()
{
FILE *fp; // 文件指针
struct Student stu; // 学生变量
// 以二进制追加模式打开文件
// "ab"表示在二进制文件末尾追加数据
fp = fopen("students.dat", "ab");
if (fp == NULL)
{
printf("文件打开失败!\n");
return;
}
// 输入学生信息
printf("请输入学号:");
scanf("%d", &stu.id);
printf("请输入姓名:");
scanf("%s", stu.name);
printf("请输入成绩:");
scanf("%f", &stu.score);
// 将学生信息以二进制方式写入文件
fwrite(&stu, sizeof(struct Student), 1, fp);
printf("学生信息添加成功!\n");
fclose(fp); // 关闭文件
}
// 显示所有学生信息
void displayAll()
{
FILE *fp; // 文件指针
struct Student stu; // 临时存储读取的学生信息
int count = 0; // 学生计数器
// 以二进制只读模式打开文件
fp = fopen("students.dat", "rb");
if (fp == NULL)
{
printf("文件打开失败或没有数据!\n");
return;
}
printf("\n%-6s %-10s %s\n", "学号", "姓名", "成绩");
printf("------------------------------\n");
// 循环读取文件中的学生信息
// fread返回成功读取的块数,1表示成功读取1个学生
while (fread(&stu, sizeof(struct Student), 1, fp) == 1)
{
printf("%-6d %-10s %.1f\n", stu.id, stu.name, stu.score);
count++; // 计数器加1
}
if (count == 0)
{
printf("(暂无学生信息)\n");
}
else
{
printf("------------------------------\n");
printf("共 %d 名学生\n", count);
}
fclose(fp); // 关闭文件
}
// 按学号查找学生
void searchById()
{
FILE *fp; // 文件指针
struct Student stu; // 临时存储读取的学生信息
int searchId; // 要查找的学号
int found = 0; // 是否找到的标志,0表示未找到
// 以二进制只读模式打开文件
fp = fopen("students.dat", "rb");
if (fp == NULL)
{
printf("文件打开失败或没有数据!\n");
return;
}
printf("请输入要查找的学号:");
scanf("%d", &searchId);
// 遍历文件中的所有学生记录
while (fread(&stu, sizeof(struct Student), 1, fp) == 1)
{
// 判断当前学生的学号是否匹配
if (stu.id == searchId)
{
printf("\n找到该学生:\n");
printf("学号:%d\n", stu.id);
printf("姓名:%s\n", stu.name);
printf("成绩:%.1f\n", stu.score);
found = 1; // 设置标志为已找到
break; // 找到后退出循环
}
}
// 如果遍历完所有记录仍未找到
if (!found)
{
printf("未找到学号为 %d 的学生!\n", searchId);
}
fclose(fp); // 关闭文件
}
// 修改学生成绩
void modifyScore()
{
FILE *fp; // 文件指针
struct Student stu; // 临时存储读取的学生信息
int searchId; // 要修改的学号
float newScore; // 新成绩
int found = 0; // 是否找到的标志
long pos; // 记录找到时的文件位置
// 以二进制读写模式打开文件
// "r+b"表示以读写方式打开已有的二进制文件
fp = fopen("students.dat", "r+b");
if (fp == NULL)
{
printf("文件打开失败或没有数据!\n");
return;
}
printf("请输入要修改成绩的学号:");
scanf("%d", &searchId);
// 遍历文件查找目标学生
while (fread(&stu, sizeof(struct Student), 1, fp) == 1)
{
if (stu.id == searchId)
{
printf("找到学生:%s,当前成绩:%.1f\n", stu.name, stu.score);
printf("请输入新成绩:");
scanf("%f", &newScore);
// 记录当前文件位置(即该学生记录的末尾)
pos = ftell(fp);
// 将文件指针回退到该学生记录的起始位置
fseek(fp, pos - sizeof(struct Student), SEEK_SET);
// 更新成绩
stu.score = newScore;
// 将修改后的数据写回文件(覆盖原数据)
fwrite(&stu, sizeof(struct Student), 1, fp);
printf("成绩修改成功!\n");
found = 1;
break;
}
}
if (!found)
{
printf("未找到学号为 %d 的学生!\n", searchId);
}
fclose(fp); // 关闭文件
}
八、知识点总结表
文件操作函数速查表
| 函数名 | 功能 | 所属分类 |
|---|---|---|
fopen() |
打开文件 | 基本操作 |
fclose() |
关闭文件 | 基本操作 |
fputc() |
写入一个字符 | 字符读写 |
fgetc() |
读取一个字符 | 字符读写 |
fputs() |
写入一个字符串 | 字符串读写 |
fgets() |
读取一行字符串 | 字符串读写 |
fprintf() |
格式化写入 | 格式化读写 |
fscanf() |
格式化读取 | 格式化读写 |
fwrite() |
写入数据块(二进制) | 数据块读写 |
fread() |
读取数据块(二进制) | 数据块读写 |
fseek() |
定位文件位置 | 文件定位 |
rewind() |
重置到文件开头 | 文件定位 |
ftell() |
获取当前位置 | 文件定位 |
remove() |
删除文件 | 文件管理 |
rename() |
重命名文件 | 文件管理 |
feof() |
检测文件末尾 | 状态检测 |
ferror() |
检测读写错误 | 状态检测 |
clearerr() |
清除错误/结束标志 | 状态检测 |
文件操作的标准流程
1. 声明文件指针:FILE *fp;
2. 打开文件:fp = fopen("文件名", "模式");
3. 判断是否打开成功:if (fp == NULL) ...
4. 读写操作:fputc/fgetc/fputs/fgets/fprintf/fscanf/fread/fwrite
5. 关闭文件:fclose(fp);
核心原则:每次打开文件后都要判断是否成功,使用完毕后必须关闭文件。这是文件操作中最基本也最重要的规范。