C语言 ——— 文件操作的核心概念与函数使用
目录
文件指针
核心本质
文件的打开和关闭
关键:打开模式(mode)
使用步骤(必做)
注意事项
相对路径和绝对路径
一、相对路径:依赖当前程序的工作目录
1. 最简单的相对路径:直接写文件名
2. 带层级的相对路径:通过 . 和 .. 表示目录关系
二、绝对路径:从根目录开始的完整路径
补充:路径中的特殊符号说明
文件的顺序读写
fputc 函数
fgetc 函数
fputs 函数
fgets 函数
fprintf 函数
fscanf 函数
fwrite 函数
fread 函数
文件的随机读写
fseek 函数
ftell 函数
rewind 函数
文件读取结束的判定 - feof
1. 函数原型
2. 参数解析
3. 返回值
二、核心作用:区分 “文件结束” 与 “读写错误”
三、常见误区:不要用 feof 作为循环条件
四、正确用法示例
文件指针
核心本质
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 <stdio.h>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 <stdio.h>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 <stdio.h>
#include <string.h>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 <stdio.h>int main() {FILE* fp = fopen("test.txt", "r");if (fp == NULL) {printf("文件打开失败\n");return 1;}int c; // 用int接收fgetc返回值(需容纳EOF)// 正确逻辑:先读取,再判断是否为EOFwhile ((c = fgetc(fp)) != EOF) {putchar(c); // 输出读取到的字符}// 读写结束后,用feof判断原因if (feof(fp)) {printf("\n读取结束:已到达文件末尾\n"); // 正常结束} else {printf("\n读取失败:发生I/O错误\n"); // 异常错误(可用ferror进一步确认)}fclose(fp);return 0;
}