C语言——文件操作
C语言通过标准库函数实现文件操作,基于文件指针(FILE*)进行读写。文件指针指向一个包含文件信息的结构,这些信息包括:缓冲区的位置、缓冲区中当前字符的位置、文件的读或写状态、是否出错或是否已经达到文件结尾等等。用户不需关注这些细节,因为<stdio.h>中已经定义了一个包含这些信息的结构FILE。在程序中只需按照下列方式声明一个文件指针即可:
FILE* fp;
1、文件类型
-
文本文件: 以ASCII编码存储的文件,如 .txt。确切地说,英文、数字等字符存储的是ASCII码,而汉字存储的是机内码。文本文件除了存储文件有效字符信息(包括能用ASCII码字符表示的回车、换行等信息)外,不能存储其他任何人信息。换行符可能被转换(如Windows的 \r\n 转为 \n)。
-
二进制文件:由文件在外部设备的存放形式为二进制而得名。直接存储数据内存映像,如图片、音频。无格式转换,读写高效,并且对于一些较精确的数据,使用二进制储存不会造成有效位的丢失。
2、打开与关闭文件
2.1、打开文件——fopen()
FILE *fopen(char *name, char *mode)
fopen 函数返回一个指向结构 FILE 的指针。注意,FILE 像 int 一样是一个类型名,而不是结构标记,它是通过 typedef 定义的。
在程序中,可以这样调用 fopen 函数:
/* 模式:r(读),(写),a(追加),可加b(二进制)或 +(读写) */
FILE* fp = fopen("file.txt", "r");
if (fp == NULL) {
perror("Error opening file!");
exit(1);
}
fopen 的第一个参数是一个字符串,它包含文件名。第二个参数时访问模式,也是一个字符串,用于指定文件的使用方式。允许的模式包括:读("r")、写("w")及追加("a")。某些系统还区分文本文件和二进制文件,对二进制文件的访问还需要在模式字符串中增加字符"b"。
如果打开一个不存在的文件用于写或追加,该文件将被创建(如果可能的话)。当以写方式打开一个已存在的文件时,该文件原来的内容将被覆盖。但是,如果以追加方式打开一个文件,则该文件原来的内容将保留不变。
读一个不存在的文件会导致错误,其他一些操作也可能会导致错误,比如试图读取一个无读权限的文件。如果发生错误,fopen 将返回 NULL。
2.2、关闭文件——fclose()
int fclose(FILE *fp)
执行和 fopen 相反的操作,成功返回 0。它断开由 fopen 函数建立的文件指针和外部名之间的连接,并释放文件指针以供其他文件使用。因为大多数操作系统都限制了一个程序可以同时打开的文件数,所以当文件指针不再需要时就应该释放。
对输出文件执行 fclose 还有另外一个原因:它将把每个缓冲区中由 puts 函数正在收集的输出写到文件中。当程序正常终止时,程序会自动为每个打开的文件调用 fclose 函数。
fclose(fp); /* 关闭后避免野指针 */
3、文本文件读写
3.1、单个字符读写
文件被打开时,就需要考虑采用哪种方法对文件进行读写。有多种方法可以考虑,其中 getc 和 putc 函数最为简单。
getc 从文件中返回下一个字符,它需要直到文件指针,以确定对哪个文件执行操作:
int getc(FILE *fp)
getc 函数返回 fp 指向的输入流中的下一个字符。如果到达文件尾或出现错误,该函数将返回EOF。
putc 是一个输出函数,其原型如下所示:
int putc(int c, FILE *fp)
该函数将字符 c 写入到 fp 指向的文件中,并返回写入的字符。如果发生错误,则返回 EOF。
3.2、格式化读写
对于文件的格式化输入或输出,可以使用函数 fscanf 和 fprintf。它们与 scanf 和 printf 函数的区别仅仅在于它们的第一个参数是一个指向所要读写的文件的指针,第二个参数是格式串。如下所示:
int fscanf(FILE *fp, char *format, ...)
int fprintf(FILE *fp, char *format, ...)
/* 写入 */
int year = 2025, month = 3, day = 1;
fprintf(fp, "Date: %d-%d-%d\n", year, month, day);
/* 读取 */
int a, b;
fscanf(fp, "%d %d", &a, &b); /* 遇到空格/换行停止 */
3.3、行读写
标准库提供了一个输入函数fgets,其函数原型为:
char *fgets(char *line, int maxline, FILE *fp)
fgets 函数从 fp 指向的文件中读取下一个输入行(包括换行符),并将它存放在字符数组line中,它最多可读取 maxline-1 个字符。读取的行将以 '\0' 结尾保存到数组中。通常情况下,fgets返回line,但如果遇到了文件结尾或发生了错误,则返回NULL。
输出函数 fputs 将一个字符串(不需要包含换行符)写入到一个文件中:
int fputs(char *line, FILE *fp)
如果发生错误,该函数将返回EOF,否则返回一个非负值。
char buffer[256];
/* 读取一行(含换行符) */
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
/* 写入字符串(不自动添加换行) */
fputs("Hello World!\n", fp);
4、二进制文件读写
4.1、块写入——fwrite()
size_t fwrite(const void *ptr,
size_t size,
size_t count,
FILE *stream);
参数说明:
- ptr:待写入数据的指针(如数组、结构体地址)。
- size:单个元素的大小(单位:字节)。
- count:要写入的元素个数。
- stream:文件指针。
返回值:成功写入的元素个数(若与 count 不符,可能发生写入错误)。
4.2、块读取——fread()
size_t fread(const void *ptr,
size_t size,
size_t count,
FILE *stream);
参数说明同上,只是写入换作读取来理解。 其返回值是:成功读取的元素个数(若小于 count,可能到达文件末尾或发生错误)。
struct Student {
int id;
char name[20];
float score;
};
struct Student st;
/* 写入结构体 */
fwrite(&st, sizeof(struct Student), 1, fp);
/* 读取结构体 */
fread(&st, sizeof(struct Student), 1, fp);
参数:数据地址、元素大小、元素个数、文件指针。返回成功读写的元素个数。
5、文件定位
5.1、移动文件指针——fseek()
int fseek(FILE *stream, long offset, int origin);
参数说明:
- stream:文件指针。
- offset: 偏移量(字节数,可正可负)。
- origin:基准位置(SEEK_SET文件头、SEEK_CUR当前位置、SEEK_END文件尾)。
返回值:成功返回 0,失败返回非 0。
fseek(fp, 100, SEEK_SET); /* 跳转到文件第100字节处 */
fseek(fp, 0, SEEK_END); /* 到文件末尾 */
5.2、获取当前偏移量——ftell()
long ftell(FILE *stream);
返回值:当前指针位置(相对于文件头的字节偏移量)。
long size = ftell(fp); /* 文件大小(需先fseek到末尾) */
5.3、重置指针到文件头——rewind()
rewind(fp); /* 等价于fseek(fp, 0, SEEK_SET) */
6、错误处理
6.1、stderr 和 exit
如果因为某种原因而造成其中的一个文件无法访问,相应的诊断信息要在该连接的输出的末尾才能打印出来。当输出到屏幕时,这种处理方法上可以接受,但如果输出到一个文件或通过管道输出到另一个程序时,就无法接受了。
为更好处理此类情况,另一个输出流以与 stdin 和 stdout 相同的方式分派给程序,即 stderr。即使对标准输出进行了重定向,写到 stderr 中的输出通常也会显示在屏幕上。
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE* fp = fopen("nonexistfile.txt", "r");
if (fp == NULL) {
/* 使用 stderr 输出错误信息(立即显示,不受缓冲区影响) */
fprintf(stderr, "错误,无法打开文件!\n");
/* 使用 perror 输出系统错误详细信息(自动附加错误原因) */
perror("具体原因: ");
exit(1);
}
/* 正常流程输出到 stdout */
printf("文件已成功打开!\n");
fclose(fp);
return 0;
}
上述示例中,使用到了标准库函数 exit,当该函数被调用时,它将终止调用程序的执行。任何调用该程序的进程都可以获取 exit 的参数值,因此,可通过另一个将该程序作为子进程的程序来测试该程序是否成功。一般返回值 0 表示一切正常,而非 0 返回值通常表示出现了异常情况。exit 为每个已打开的输出文件调用 fclose 函数,以将缓冲区中的所有输出写到相应的文件中。
在主程序 main 中,语句 return expr 等价于 exit(expr)。但是使用函数 exit 有一个优点,它可以从其他函数中调用。
6.2、检查返回值
使用fopen、fread等函数的返回值检查。例如, fread 函数的返回值是:成功读取的元素个数(若小于 count,可能到达文件末尾或发生错误)。故根据 fread 函数的返回值写一个基本检查的程序示例:
#define BUF_SIZE 128
char buffer[BUF_SIZE];
size_t read_items = fread(buffer, sizeof(char), BUF_SIZE, fp);
if (read_items < BUF_SIZE) {
if (feof(fp)) {
printf("已经到达文件末尾!\n");
}
if (ferror(fp)) {
perror("读取过程中发生错误!\n");
clearerr(fp); /* 清楚错误标志 */
}
}
6.3、feof()——检测文件结束
如果指定的文件到达文件结尾的时候,feof()将返回一个非 0 值。
/* 检测文件结束 */
if (feof(fp)) {
printf("End of file reached.\n");
}
6.4、ferror()——检测文件错误
如果流 fp 中发生错误,则函数 ferror 返回一个非 0 值。尽管输出错误很少出现,但还是存在的(例如,当磁盘满时)。所以一个合格的程序应该检查这种类型的错误:
/* 检测文件错误 */
if (ferror(fp)) {
printf("File error occurred.\n");
}
7、综合示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define _CRT_SECURE_NO_WARNINGS 1 // 确保在VS中可以使用scanf
/* 学生结构体——用于二进制文件示例 */
typedef struct Student {
int id;
char name[20];
float score;
} Student;
/* 文本文件操作 */
void textFileDemo()
{
/* 文本文件写入 */
FILE* fp = fopen("data.txt", "w");
if (fp == NULL) {
perror("Failed to create a text file!");
return;
}
// 使用 fprintf 写入格式化数据
fprintf(fp, "===== 学生成绩单 =====\n");
fprintf(fp, "%-8s %-10s %s\n", "ID", "姓名", "成绩");
fprintf(fp, "------------------------\n");
for (int i = 0; i < 3; i++) {
fprintf(fp, "%-8d 学生%4d %.1f\n", 1001 + i, i + 1, 80.5 + i * 5);
}
fclose(fp); /* 操作完成后及时 fclose */
/* 文本文件读取 */
printf("\n读取文本文件内容:\n");
fp = fopen("data.txt", "r");
if (!fp) {
perror("Failed to open a text file!");
return;
}
// 使用 fgets 逐行读取文件内容
char buffer[256];
while (fgets(buffer, sizeof(buffer), fp)) {
printf("%s", buffer);
}
fclose(fp);
}
/* 二进制文件操作 */
void binaryFileDemo()
{
/* 二进制文件写入 */
Student students[] = {
{"1001", "Klein", 87.5},
{"1002", "Bob", 90.0},
{"1003", "Alice", 92.5},
};
FILE* fp = fopen("student.dat", "wb");
if (!fp) {
perror("Failed to create a binary file!");
return;
}
// 使用 fwrite 写入结构体数组
fwrite(students, sizeof(Student), 3, fp);
fclose(fp);
/* 二进制文件读取 */
printf("\n读取二进制文件内容:\n");
fp = fopen("student.dat", "rb");
if (!fp) {
perror("Failed to open a binary file!");
return;
}
// 使用 fread 读取结构体数据
Student st;
printf("%-8s %-10s %s\n", "ID", "姓名", "成绩");
while (fread(&st, sizeof(Student), 1, fp) == 1) {
printf("%-8d %-10s %.1f\n", st.id, st.name, st.score);
}
fclose(fp);
}
/* 文件定位操作 */
void filePositionDemo()
{
FILE* fp = fopen("data.txt", "r+");
if (!fp) {
perror("Failed to open a file!");
return;
}
/* 使用 fseek 定位到文件末尾并追加内容 */
fseek(fp, 0, SEEK_END);
fprintf(fp, "\n备注:以上为模拟数据!");
/* 使用 ftell 获取文件大小 */
long size = ftell(fp);
printf("\n文件大小:%ld bytes\n", size);
/* 使用 rewind 回到文件开头读取 */
rewind(fp);
char firstLine[100];
fgets(firstLine, sizeof(firstLine), fp);
printf("首行内容:%s", firstLine);
fclose(fp);
}
/* 错误处理示例 */
void errorHandleDemo()
{
/* 检查 fopen 返回值 */
FILE* fp = fopen("nonexistent.txt", "r");
if (!fp) {
perror("\n错误信息示例"); /* 使用 perror 输出错误信息 */
return;
}
/* 强制引发读取错误 */
int ch;
while ((ch = fgetc(fp)) != EOF);
// 使用 feof 和 ferror 检测状态
if (feof(fp)) {
printf("\n正常到达文件末尾");
}
if (ferror(fp)) {
printf("\n发生文件读取错误");
}
fclose(fp);
}
int main()
{
textFileDemo();
binaryFileDemo();
filePositionDemo();
errorHandleDemo();
return 0;
}