梳理一下 C 语言中文件读取方法的基础语法和要点。
核心概念
在 C 语言中,文件操作是通过 文件指针 (File Pointer) 和 C 标准库 (stdio.h) 提供的一系列函数来完成的。基本流程如下:
定义文件指针: 使用 FILE * 类型定义一个指针变量。
打开文件: 使用 fopen() 函数打开一个物理文件,并将其与文件指针关联起来。你需要指定文件名和 打开模式 (例如,只读)。
读取数据: 使用不同的读取函数(如 fgetc, fgets, fscanf, fread)从文件中读取数据。
处理数据: 对读取到的数据进行你需要的操作。
判断文件结束: 在读取循环中,需要判断是否已经到达文件末尾 (EOF - End Of File)。
关闭文件: 使用 fclose() 函数关闭文件,释放资源。
错误处理: 在打开文件和读取/写入过程中,需要检查可能发生的错误。
基础语法和关键函数
1. 包含头文件
进行文件操作前,必须包含标准输入输出头文件:
#include
2. 定义文件指针
FILE *fp; // 或者 FILE *infile, *pFile 等有意义的名称
FILE 是一个在 stdio.h 中定义的结构体类型,用于存储文件的相关信息(如缓冲区、当前位置、错误状态等)。我们通过操作指向 FILE 结构的指针来进行文件操作。
3. 打开文件 (fopen)
fp = fopen("文件名", "打开模式");
文件名: 一个字符串,表示要打开的文件的路径和名称(例如 "data.txt", "C:\\Users\\Me\\Documents\\input.log")。
打开模式: 一个字符串,指定文件的打开方式。对于读取,常用的模式有:
"r": 只读 (Read)。文件必须存在,否则打开失败。从文件开头读取。
"rb": 只读,二进制模式 (Read Binary)。同上,但用于读取二进制文件(如图片、音频)。
"r+": 读写 (Read Plus)。文件必须存在。可以读取和写入,从文件开头开始。
"rb+" 或 "r+b": 读写,二进制模式 (Read Binary Plus)。同上,用于二进制文件。
返回值:
成功:返回一个指向 FILE 结构的指针 (即文件指针)。
失败:返回 NULL。 必须检查 fopen 的返回值!
示例:打开文件并检查错误
#include
#include
int main() {
FILE *fp;
const char *filename = "mydata.txt";
fp = fopen(filename, "r"); // 尝试以只读模式打开
if (fp == NULL) {
perror("Error opening file"); // perror 会打印错误信息和系统错误原因
// 或者 fprintf(stderr, "无法打开文件 %s\n", filename);
return 1; // 或者 exit(EXIT_FAILURE); // 表示程序异常退出
}
printf("文件 %s 打开成功!\n", filename);
// ... 接下来进行文件读取操作 ...
// 关闭文件(稍后讲解)
fclose(fp);
return 0; // 程序正常退出
}
4. 读取文件内容
有多种方法可以读取文件内容,根据需要选择:
a) 读取单个字符 (fgetc)
一次读取一个字符。
返回读取到的字符 (作为 int 类型)。
如果到达文件末尾或发生错误,返回 EOF (一个在 stdio.h 中定义的特殊整数常量,通常是 -1)。
#include
int main() {
FILE *fp;
int ch; // 注意是 int 类型,用来接收 fgetc 的返回值,包括 EOF
fp = fopen("mydata.txt", "r");
if (fp == NULL) {
perror("Error opening file");
return 1;
}
printf("文件内容:\n");
// 循环读取,直到 fgetc 返回 EOF
while ((ch = fgetc(fp)) != EOF) {
putchar(ch); // 将读取到的字符打印到屏幕
// 或者 (char)ch 进行处理
}
// 检查是读取结束还是发生错误 (可选但推荐)
if (ferror(fp)) {
perror("Error reading file");
} else if (feof(fp)) {
printf("\n文件读取结束 (EOF reached).\n");
}
fclose(fp);
return 0;
}
b) 读取一行字符串 (fgets)
读取一整行(直到换行符 \n)或指定的最大字符数。
推荐使用,因为它更安全,可以防止缓冲区溢出。
会在读取的字符串末尾自动添加空字符 \0。
会 读取并存储行末的换行符 \n (如果空间足够)。
char *fgets(char *str, int n, FILE *stream);
str: 用于存储读取内容的字符数组(缓冲区)。
n: 最多读取 n-1 个字符(留一个位置给 \0)。
stream: 文件指针。
返回值:
成功:返回 str (指向缓冲区的指针)。
到达文件末尾(且未读取任何字符)或发生错误:返回 NULL。
#include
#define BUFFER_SIZE 256 // 定义缓冲区大小
int main() {
FILE *fp;
char buffer[BUFFER_SIZE];
fp = fopen("mydata.txt", "r");
if (fp == NULL) {
perror("Error opening file");
return 1;
}
printf("文件内容 (按行读取):\n");
// 循环读取,直到 fgets 返回 NULL
while (fgets(buffer, BUFFER_SIZE, fp) != NULL) {
printf("%s", buffer); // 直接打印,因为 buffer 可能包含换行符
}
// 检查是读取结束还是发生错误 (可选但推荐)
if (ferror(fp)) {
perror("Error reading file");
} else if (feof(fp)) {
printf("\n文件读取结束 (EOF reached).\n");
}
fclose(fp);
return 0;
}
c) 按格式读取 (fscanf)
类似于 scanf,但从文件读取。
根据指定的格式字符串解析数据。
注意: 对于字符串读取,fscanf 遇到空白字符(空格、制表符、换行符)会停止,可能不适合读取整行文本。处理格式不固定的文本时要小心。
int fscanf(FILE *stream, const char *format, ...);
stream: 文件指针。
format: 格式控制字符串 (同 scanf)。
...: 接收数据的变量地址。
返回值:
成功:返回成功匹配并赋值的项数。
未匹配任何项(可能由于格式不符或到达文件末尾):返回 0 或 EOF。
发生读取错误:返回 EOF。
#include
// 假设 mydata.txt 内容是:
// Name John Age 30 Score 95.5
// Name Jane Age 25 Score 88.0
int main() {
FILE *fp;
char name_label[10], name[50], age_label[10];
int age;
char score_label[10];
double score;
fp = fopen("mydata.txt", "r");
if (fp == NULL) {
perror("Error opening file");
return 1;
}
printf("从文件读取结构化数据:\n");
// 循环读取,直到 fscanf 返回值不等于预期的项数 (这里是 6)
// 或者直接判断返回值是否等于 EOF
while (fscanf(fp, "%s %s %s %d %s %lf",
name_label, name, age_label, &age, score_label, &score) == 6)
{
printf("读取到: Name=%s, Age=%d, Score=%.1f\n", name, age, score);
}
// 检查是读取结束还是发生错误/格式不匹配
if (ferror(fp)) {
perror("Error reading file");
} else if (feof(fp)) {
printf("文件读取结束或格式不再匹配.\n");
} else {
// 如果循环是因为格式不匹配而停止的,这里会被执行
printf("文件格式不匹配或读取意外终止.\n");
}
fclose(fp);
return 0;
}
d) 读取二进制数据 (fread)
用于读取指定大小和数量的数据块,通常用于非文本文件(二进制文件)。
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr: 指向用于存储读取数据的内存块的指针(缓冲区)。
size: 要读取的每个数据项的大小(字节)。
nmemb: 要读取的数据项的数量。
stream: 文件指针。
返回值: 返回成功读取的数据项的数量(不是字节数!)。如果发生错误或到达文件末尾,返回值可能小于 nmemb。
5. 关闭文件 (fclose)
断开文件指针与物理文件的连接。
将缓冲区中剩余的数据(如果有的话,主要针对写入)刷新到文件中。
释放与文件关联的系统资源。
必须在使用完文件后调用 fclose!
int fclose(FILE *stream);
stream: 要关闭的文件指针。
返回值:
成功:返回 0。
失败:返回 EOF (通常表示写入错误,如磁盘已满)。
#include
int main() {
FILE *fp;
fp = fopen("mydata.txt", "r");
if (fp == NULL) {
perror("Error opening file");
return 1;
}
// ... 读取操作 ...
if (fclose(fp) != 0) {
perror("Error closing file");
// 注意:即使关闭失败,文件指针 fp 也可能不再有效
} else {
printf("文件成功关闭。\n");
}
// fp = NULL; // 可选:将指针设为 NULL,防止后续误用
return 0;
}
6. 文件结束与错误检查
feof(FILE *stream): 检查文件流的文件结束指示符是否被设置。注意:feof 只有在一次读取操作尝试读取超出文件末尾之后才会返回真 (非零值)。不要用 while(!feof(fp)) 作为读取循环的条件,应该在读取操作失败后使用 feof 来判断失败的原因是否是到达文件末尾。
ferror(FILE *stream): 检查文件流的错误指示符是否被设置。如果之前的I/O操作发生了错误,返回真 (非零值)。
正确的文件读取循环模式:
// 使用 fgetc
int ch;
while ((ch = fgetc(fp)) != EOF) {
// 处理 ch
}
// 循环结束后检查是 EOF 还是 error
if (ferror(fp)) { /* handle error */ }
else { /* EOF reached */ }
// 使用 fgets
char buffer[SIZE];
while (fgets(buffer, SIZE, fp) != NULL) {
// 处理 buffer
}
// 循环结束后检查是 EOF 还是 error
if (ferror(fp)) { /* handle error */ }
else { /* EOF reached (or possibly partial read on last line) */ }
// 使用 fscanf (假设期望读取 N 个项目)
while (fscanf(fp, "...", ...) == N) {
// 处理读取到的数据
}
// 循环结束后检查是 EOF 还是 error/format mismatch
if (ferror(fp)) { /* handle error */ }
else if (feof(fp)) { /* EOF reached cleanly or after last successful match */ }
else { /* format mismatch */ }
// 使用 fread (假设期望读取 count 个项目)
size_t items_read;
while ((items_read = fread(buffer, item_size, count, fp)) == count) {
// 处理 buffer 中的 count 个项目
}
// 处理最后一次可能不完整的读取 (items_read < count)
if (items_read > 0) {
// 处理 buffer 中的 items_read 个项目
}
// 循环结束后检查是 EOF 还是 error
if (ferror(fp)) { /* handle error */ }
else { /* EOF reached */ }
要点总结
包含
使用 FILE * 定义文件指针。
使用 fopen() 打开文件,指定文件名和读取模式 ("r", "rb" 等)。
必须检查 fopen() 的返回值是否为 NULL。
根据需要选择合适的读取函数 (fgetc, fgets, fscanf, fread)。
读取循环的条件应基于读取函数的返回值,而不是 feof()。
在读取循环结束后,使用 feof() 和 ferror() 区分文件结束和读取错误。
对于 fgets,注意提供足够大的缓冲区并处理可能存在的行末换行符。
对于 fscanf,注意其处理空白字符的行为和返回值(匹配项数)。
使用 fclose() 关闭文件以释放资源,并检查其返回值。
始终进行错误处理。