C语言 ——— 档案操作的核心概念与函数使用
目录
文件指针
核心本质
FILE是标准库定义的一个结构体(具体实现因编译器 / 系统而异),内部封装了文件操作所需的关键信息:
- 文件描述符(底层操作系统标识文件的整数);
- 缓冲区(用于减少磁盘 I/O 次数的内存区域);
- 文件位置指针(记录当前读写位置);
- 错误标志、文件打开模式(如 "r"、"wb")等元信息。
FILE*指针本身不直接指向物理文件,而是通过指向这个FILE结构体,间接关联到具体的文件,成为所有文件操作的 “桥梁”。
文件的打开和关闭
fopen 是 C 语言标准库(stdio.h)中用于打开文件的核心函数,作用是建立程序与文件之间的连接,返回一个 FILE* 指针(文件指针)供后续读写操作使用。
FILE* fopen(const char* filename, const char* mode);
- 参数 1(filename):字符串,指定要打开的文件路径(可以是相对路径或绝对路径,如
"test.txt"或"/home/user/data.dat")。 - 参数 2(mode):字符串,指定文件的打开模式(决定读写权限、文件不存在时的行为等)。
- 返回值:成功打开时返回指向
FILE结构体的指针(FILE*);失败时返回NULL(如文件不存在且模式不允许创建、权限不足等)。
关键:打开模式(mode)
mode 决定了文件的操作权限,常用模式如下:
| 模式 | 含义 |
|---|---|
"r" | 只读打开文本文件。文件必须存在,否则打开失败。 |
"w" | 只写打开文本文件。文件不存在则创建;文件存在则清空原有内容。 |
"a" | 追加打开文本文件。文件不存在则创建;写入时数据追加到文件末尾(不覆盖原有内容)。 |
"r+" | 读写打开文本文件。文件必须存在,可读写原有内容。 |
"w+" | 读写打开文本文件。文件不存在则创建;存在则清空,可读写。 |
"a+" | 读写打开文本文件。文件不存在则创建;读时可访问全部内容,写时只能追加。 |
- 若操作二进制文件(如图片、音频),需在模式后加
"b"(如"rb"、"wb+"),避免系统对换行符等文本特殊字符的自动转换。
使用步骤(必做)
- 调用
fopen打开文件,传入文件名和模式; - 检查返回的
FILE*指针是否为NULL(打开失败的处理,如打印错误信息); - 通过返回的指针进行文件操作(如
fprintf写入、fscanf读取等); - 操作完成后用
fclose关闭文件(释放资源,确保数据刷新到磁盘)。
#include
int main() {
// 打开文件(以只写模式打开文本文件,不存在则创建)
FILE* fp = fopen("test.txt", "w");
// 检查打开是否成功(关键!)
if (fp == NULL)
{
perror("fopen failed"); // 打印错误原因(如权限不足)
return 1;
}
// 写入内容
fprintf(fp, "Hello, fopen!");
// 关闭文件(必须!)
fclose(fp);
fp = NULL;
return 0;
}
注意事项
- 错误检查不可少:
fopen失败时返回NULL,必须判断,否则后续操作会导致程序崩溃。 - 及时关闭文件:
fclose(fp)不仅释放资源,还会将缓冲区数据强制刷新到磁盘(避免数据丢失)。 - 模式匹配需求:根据实际读写需求选择模式(如不想覆盖原有内容则用
"a"而非"w")。 - 路径格式:不同系统路径分隔符不同(Linux 用
/,Windows 用\或/),注意兼容性。
相对路径和绝对路径
一、相对路径:依赖当前程序的工作目录
相对路径是以程序运行时的 “当前工作目录” 为起点的路径,无需从根目录开始描述,仅通过层级关系(如子目录、当前目录、上一级目录)即可定位文件,特点是简洁但依赖当前位置。
1. 最简单的相对路径:直接写文件名
FILE* fp = fopen("test.txt", "w");
- 它表示 “在程序当前的工作目录下” 查找或创建
test.txt文件; - “当前工作目录” 通常是程序可执行文件(
.exe)所在的目录,或运行程序时的命令行所在目录(具体取决于编译环境和运行方式)。
2. 带层级的相对路径:通过 . 和 .. 表示目录关系
FILE* fp = fopen(".\\Debug\\test.txt", "w");
".\\"表示 “当前目录”(与程序工作目录相同);Debug是当前目录下的子目录;- 整体路径表示 “当前目录 → Debug 子目录 → test.txt 文件”。
- 若要定位 “上一级目录” 中的文件,可使用
..\\(如..\\test.txt表示 “当前目录的上一级目录下的 test.txt”)。
二、绝对路径:从根目录开始的完整路径
绝对路径是从文件系统的 “根目录” 开始的完整路径,完整描述了文件的精确位置,不依赖程序的当前工作目录,无论程序在哪里运行,都能准确定位文件,但路径较长且跨系统(如 Windows 和 Linux)格式有差异。
FILE* fp = fopen("C:\\Users\\32937\\Desktop\\test.txt", "w");
- 从根目录
C:开始,依次经过Users目录 →32937目录 →Desktop目录,最终定位到test.txt; - 路径中的
\\是 C 语言中的特殊写法:单个\在 C 中会被视为 “转义字符的开始”(如\n表示换行),因此需要用\\表示一个实际的反斜杠,避免解析错误。
补充:路径中的特殊符号说明
\\(Windows)或/(Linux/macOS):用于分隔目录层级(Windows 习惯用\,但 C 语言中需写为\\;Linux 统一用/,无需转义);.:表示当前目录;..:表示上一级目录。
文件的顺序读写
| 函数原型 | 功能描述 | 参数说明 | 返回值 | 备注 |
|---|---|---|---|---|
int fputc(int ch, FILE* stream) | 向文件写入一个字符 | - ch:要写入的字符(ASCII 码)- stream:文件指针(如fp) | 成功:返回写入的字符(ch)失败:返回EOF(-1) | 顺序写入,每次写一个字符,指针自动后移 |
int fgetc(FILE* stream) | 从文件读取一个字符 | - stream:文件指针 | 成功:返回读取的字符(ASCII 码)失败 / 文件尾:返回EOF(-1) | 顺序读取,每次读一个字符,指针自动后移;读取结束需用feof判断是否到文件尾 |
int fputs(const char* str, FILE* stream) | 向文件写入一个字符串 | - str:要写入的字符串(以'\0'结尾)- stream:文件指针 | 成功:返回非负数失败:返回EOF(-1) | 不自动写入换行符,需手动加'\n';'\0'不写入文件 |
char* fgets(char* str, int n, FILE* stream) | 从文件读取一个字符串(最多 n-1 个字符) | - str:存储读取结果的缓冲区- n:最大读取字符数(含'\0')- stream:文件指针 | 成功:返回str(缓冲区地址)失败 / 文件尾:返回NULL | 读取到换行符'\n'或达到 n-1 个字符时停止,自动在末尾加'\0';换行符会被读入 |
int fprintf(FILE* stream, const char* format, ...) | 按格式化字符串向文件写入数据 | - stream:文件指针- format:格式化字符串(如"%d %s")- ...:可变参数(要写入的数据) | 成功:返回写入的字符总数失败:返回负数 | 与printf用法类似,只是输出目标是文件而非屏幕 |
int fscanf(FILE* stream, const char* format, ...) | 按格式化字符串从文件读取数据 | - stream:文件指针- format:格式化字符串- ...:可变参数(存储读取结果的地址) | 成功:返回成功匹配并赋值的参数个数失败 / 文件尾:返回EOF | 与scanf用法类似,只是输入来源是文件而非键盘;跳过空白字符(空格、换行等) |
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream) | 以二进制形式向文件写入数据 | - ptr:要写入数据的起始地址- size:单个元素的大小(字节)- count:元素个数- stream:文件指针 | 成功:返回实际写入的元素个数失败:返回值小于count(可能为 0) | 适合写入二进制数据(如结构体、数组);不处理格式,直接写内存二进制内容 |
size_t fread(void* ptr, size_t size, size_t count, FILE* stream) | 以二进制形式从文件读取数据 | - ptr:存储读取结果的缓冲区地址- size:单个元素的大小(字节)- count:要读取的元素个数- stream:文件指针 | 成功:返回实际读取的元素个数失败 / 文件尾:返回值小于count(可能为 0) | 适合读取二进制数据;读取结束需用feof判断是否到文件尾 |
fputc 函数
int main()
{
// 创建
FILE* fp = fopen("C:\\Users\\32937\\Desktop\\test.txt", "w");
if (fp == NULL)
{
perror("fopen failed");
return 1;
}
// 写入
char arr[] = "abcdefg";
int len = strlen(arr);
for (int i = 0; i < len; i++)
{
if (fputc(arr[i], fp) == EOF)
perror("fputc");
}
// 释放
fclose(fp);
fp = NULL;
return 0;
}
fgetc 函数
int main()
{
FILE* fp = fopen("C:\\Users\\32937\\Desktop\\test.txt", "r");
if (fp == NULL)
{
perror("fopen failed");
return 1;
}
while(fgetc(fp) != EOF)
{
printf("%c\n", fgetc(fp));
}
fclose(fp);
fp = NULL;
return 0;
}
fputs 函数
int main()
{
FILE* fp = fopen("C:\\Users\\32937\\Desktop\\test.txt", "w");
if (fp == NULL)
{
perror("fopen failed");
return 1;
}
if (fputs("abcdef", fp) == EOF)
{
perror("fputs");
return -1;
}
fclose(fp);
fp = NULL;
return 0;
}
fgets 函数
int main()
{
FILE* fp = fopen("C:\\Users\\32937\\Desktop\\test.txt", "r");
if (fp == NULL)
{
perror("fopen failed");
return 1;
}
char arr[10] = "";
if (fgets(arr, 7, fp) == NULL)
{
perror("fputs");
return -1;
}
printf("%s\n", arr);
fclose(fp);
fp = NULL;
return 0;
}
fprintf 函数
struct S
{
int n;
float f;
};
int main()
{
FILE* fp = fopen("C:\\Users\\32937\\Desktop\\test.txt", "w");
if (fp == NULL)
{
perror("fopen failed");
return 1;
}
struct S s = { 5,3.14 };
fprintf(fp, "%d %f", s.n, s.f);
fclose(fp);
fp = NULL;
return 0;
}
fscanf 函数
struct S
{
int n;
float f;
};
int main()
{
FILE* fp = fopen("C:\\Users\\32937\\Desktop\\test.txt", "r");
if (fp == NULL)
{
perror("fopen failed");
return 1;
}
struct S s = { 0 };
fscanf(fp, "%d %f", &(s.n), &(s.f));
printf("%d %f\n", s.n, s.f);
fclose(fp);
fp = NULL;
return 0;
}
fwrite 函数
struct S
{
int n;
float f;
};
int main()
{
FILE* fp = fopen("C:\\Users\\32937\\Desktop\\test.txt", "wb");
if (fp == NULL)
{
perror("fopen");
return -1;
}
struct S s = { 100,3.14f };
fwrite(&s, sizeof(struct S), 1, fp);
fclose(fp);
fp = NULL;
return 0;
}
fread 函数
struct S
{
int n;
float f;
};
int main()
{
FILE* fp = fopen("C:\\Users\\32937\\Desktop\\test.txt", "rb");
if (fp == NULL)
{
perror("fopen");
return -1;
}
struct S s = { 0 };
fread(&s, sizeof(struct S), 1, fp);
printf("%d %f\n", s.n, s.f);
fclose(fp);
fp = NULL;
return 0;
}
文件的随机读写
| 函数原型 | 功能描述 | 参数说明 | 返回值 | 备注 |
|---|---|---|---|---|
int fseek(FILE* stream, long offset, int origin) | 按指定 “起始位置” 和 “偏移量” 调整文件指针位置 | - stream:文件指针(需先通过fopen打开)- offset:指针偏移量(可正可负,正数向后移,负数向前移)- origin:起始位置(宏定义),可选值:- SEEK_SET:文件开头(偏移量从 0 开始)- SEEK_CUR:当前文件指针位置- SEEK_END:文件末尾 | 成功:返回 0失败:返回 非0(如偏移量超出文件范围、文件不可读 / 写等) | 随机读写的核心函数,调整指针后,后续fread/fwrite等会从新位置开始操作;二进制文件推荐使用,文本文件因换行符(\n)可能被转换(如 Windows 下\n存为\r\n),偏移量计算易出错 |
long ftell(FILE* stream) | 获取当前文件指针相对于 “文件开头” 的偏移量(字节数) | - stream:文件指针 | 成功:返回当前偏移量(long类型,正数)失败:返回 -1L(L表示长整型) | 常与fseek配合使用,例如:用fseek(stream, 0, SEEK_END)将指针移到文件尾,再用ftell获取偏移量,即可计算文件总大小 |
void rewind(FILE* stream) | 将文件指针重置到 “文件开头” 位置 | - stream:文件指针 | 无返回值 | 功能等价于 fseek(stream, 0, SEEK_SET),但代码更简洁;重置指针的同时,会清除文件的错误标志(ferror状态) |
int fsetpos(FILE* stream, const fpos_t* pos) | 按fpos_t类型的位置值,设置文件指针位置 | - stream:文件指针- pos:指向fpos_t类型变量的指针(该变量存储了通过fgetpos获取的位置信息) | 成功:返回 0失败:返回 非0 | C 标准推荐的函数,专为处理 “大文件” 设计(fpos_t是平台相关的类型,可存储比long更大的偏移量);需与fgetpos配合使用 |
int fgetpos(FILE* stream, fpos_t* pos) | 获取当前文件指针位置,存储到fpos_t类型变量中 | - stream:文件指针- pos:指向fpos_t类型变量的指针(用于接收位置信息) | 成功:返回 0失败:返回 非0 | 与fsetpos配对,解决ftell中long类型偏移量不足的问题(如 64 位文件);fpos_t的具体实现由编译器决定,无需手动修改其值 |
fseek 函数
FILE* fp = fopen("data.bin", "rb");
if (fp == NULL)
{
// 打开失败处理
}
// 从文件开头(SEEK_SET)向后移动100字节(offset=100)
// 100字节 = 25个int(每个4字节),即定位到第25个int的起始位置
int ret = fseek(fp, 100, SEEK_SET);
if (ret != 0)
{
// 定位失败处理(如文件不足100字节)
}
int num;
// 从当前指针位置(100字节处)读取1个int
fread(&num, sizeof(int), 1, fp); // 读取的是第25个int数据
ftell 函数
#include
int main() {
FILE* fp = fopen("data.bin", "rb"); // 打开二进制文件(只读)
if (fp == NULL) {
printf("文件打开失败\n");
return 1;
}
// 步骤1:将文件指针移到文件末尾(SEEK_END表示以文件尾为基准,偏移0字节)
fseek(fp, 0, SEEK_END);
// 步骤2:获取当前指针偏移量(即文件总字节数)
long file_size = ftell(fp);
if (file_size == -1L) {
printf("获取文件大小失败\n");
fclose(fp);
return 1;
}
printf("文件总大小:%ld 字节\n", file_size); // 例如:输出 "文件总大小:1024 字节"
fclose(fp);
return 0;
}
rewind 函数
#include
#include
int main() {
FILE* fp = fopen("test.txt", "r"); // 打开文本文件(只读)
if (fp == NULL) {
printf("文件打开失败\n");
return 1;
}
// 第一次读取:读取前5个字符
char buf1[6] = {0}; // 预留'\0'位置
fread(buf1, 1, 5, fp); // 读取后,指针移到第5字节处
printf("第一次读取(前5字符):%s\n", buf1); // 输出:Hello
// 检查当前指针位置(验证已偏离开头)
long pos = ftell(fp);
printf("读取后指针位置:%ld 字节\n", pos); // 输出:5(假设每个字符占1字节)
// 使用rewind重置指针到文件开头
rewind(fp);
printf("rewind后,指针位置:%ld 字节\n", ftell(fp)); // 输出:0(已回到开头)
// 第二次读取:从开头读取完整内容
char buf2[100] = {0};
fread(buf2, 1, sizeof(buf2)-1, fp);
printf("第二次读取(完整内容):%s\n", buf2); // 输出:Hello, rewind function!
fclose(fp);
return 0;
}
文件读取结束的判定 - feof
feof 是 C 语言中用于检测文件结束标志的函数,其核心作用是判断 “文件指针是否已经到达文件末尾”,帮助区分 “因文件结束导致的读写终止” 和 “因错误导致的读写终止”。它并非用于直接判断 “是否还能读写数据”,而是用于解释 “之前的读写操作失败的原因”,这是理解 feof 的关键。
1. 函数原型
int feof(FILE* stream);
2. 参数解析
FILE* stream:文件指针(需通过fopen打开文件获得,如fp),指定要检测的文件。
3. 返回值
- 若文件指针已到达文件末尾(且之前的读写操作因 “到尾” 而停止),返回非零值(通常为 1,代表 “真”);
- 若文件指针未到达末尾,返回0(代表 “假”)
二、核心作用:区分 “文件结束” 与 “读写错误”
文件读写操作(如 fread、fgetc)失败时,可能有两种原因:
- 正常原因:文件指针已到达末尾,没有更多数据可读写;
- 异常原因:发生 I/O 错误(如文件损坏、权限不足、磁盘错误等)。
feof 的核心价值就是判断失败是否因 “文件结束” 导致—— 若 feof 返回非零,说明是正常到尾;若返回 0,则需用 ferror 函数检测是否有错误(ferror 返回非零表示有错误)。
三、常见误区:不要用 feof 作为循环条件
很多初学者会错误地将 feof 作为循环条件(如 while(!feof(fp)) { ... }),这是因为对 feof 的触发时机理解有误:
feof的 “文件结束标志”不会在指针刚到达末尾时立即设置,而是在 “指针到达末尾后,再尝试进行一次读写操作” 才会设置。- 例如:文件最后一个字符被读取后,
feof仍返回 0;当再次尝试读取(此时已无数据),feof才会返回非零。
因此,直接用 feof 控制循环会导致 “多执行一次读写操作”(读取到无效数据)。
四、正确用法示例
feof 的正确使用方式是:先执行读写操作,检查其返回值是否表示失败,再用 feof 判断失败是否因 “文件结束” 导致。
代码演示:
#include
int main() {
FILE* fp = fopen("test.txt", "r");
if (fp == NULL) {
printf("文件打开失败\n");
return 1;
}
int c; // 用int接收fgetc返回值(需容纳EOF)
// 正确逻辑:先读取,再判断是否为EOF
while ((c = fgetc(fp)) != EOF) {
putchar(c); // 输出读取到的字符
}
// 读写结束后,用feof判断原因
if (feof(fp)) {
printf("\n读取结束:已到达文件末尾\n"); // 正常结束
} else {
printf("\n读取失败:发生I/O错误\n"); // 异常错误(可用ferror进一步确认)
}
fclose(fp);
return 0;
}
浙公网安备 33010602011771号