目录
1. 什么是文件
1.1 文件名
1.2 程序文件
1.3 数据文件
2. 文件的打开和关闭
2.1 流和标准流
2.1.1 流
2.1.2 标准流
2.2 文件指针
2.3 文件的打开和关闭
3. 文件的顺序读写
3.1 顺序读写函数
3.2 使用示例
3.2.1 fgetc 和 fputc
3.2.2 fgets 和 fputs
3.2.3 fscanf 和 fprintf
3.2.4 fread 和 fwrite
3.3 功能对比
3.4 scanf/fscanf/sscanf 与 printf/fprintf/sprintf
3.4.1 sscanf 和 sprintf 使用示例
4. 文件的随机读写
4.1 fseek
4.2 ftell
4.3 rewind
5. 文件读取结束的判定
5.1 文本文件读取结束的例子
5.2 二进制文件读取结束的例子
6. 文件缓冲区
:如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!
:如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并分享给更多的朋友噢~!
磁盘(硬盘)上的文件是文件。在程序设计中,一般谈及的文件有两种,即从文件功能角度分类的程序文件和数据文件。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如,c : \code\test.txt
c : \code\ 文件路径
test 文件名主干
.txt 文件后缀
程序文件包括源程序文件(后缀为 .c
)、目标文件(Windows 环境后缀为 .obj
)、可执行程序(Windows 环境后缀为 .exe
)。
根据数据的组织形式,数据文件可分为文本文件或者二进制文件。
数据在内存中以二进制形式存储,若不加转换输出到外存文件中,就是二进制文件;若要求在外存上以 ASCII 码形式存储,则需在存储前转换,以 ASCII 字符形式存储的文件就是文本文件。
字符一律以 ASCII 形式存储,数值型数据既可以用 ASCII 形式存储,也可以使用二进制形式存储。
例如整数 10000,以 ASCII 码形式输出到磁盘,在磁盘中占 5 个字节(每个字符一个字节);以二进制形式输出,在磁盘上只占 4 个字节。
程序的数据需输出到各种外部设备,也需从外部设备获取数据,不同外部设备的输入输出操作不同。为方便程序员对各种设备进行操作,抽象出了流的概念,可将流想象成流淌着字符的河。
C 程序针对数据的输入输出操作都通过流操作。
一般,要向流里写数据或从流中读取数据,都要先打开流,再进行操作。
C 语言程序启动时,默认打开 3 个流:
stdin
- 标准输入流,大多数环境中从键盘输入,scanf
函数就是从标准输入流中读取数据。stdout
- 标准输出流,大多数环境中输出至显示器界面,printf
函数就是将信息输出到标准输出流中。stderr
- 标准错误流,大多数环境中输出到显示器界面。
stdin
、stdout
、stderr
三个流的类型是 FILE*
,通常称为文件指针。
C 语言通过 FILE*
的文件指针来维护流的各种操作。
缓冲文件系统的关键概念是“文件指针”(即“文件类型指针”)。
每个使用中的文件会在内存开辟文件信息区,存放文件名字、状态、当前位置等信息,这些信息存于系统声明的 FILE 结构体变量。不同 C 编译器的 FILE 类型内容大致相同。打开文件时,系统自动创建并填充 FILE 结构体变量,用户无需关注细节。
一般用一个 FILE 指针维护该结构体变量。
FILE* pf;
声明一个 FILE 类型的指针变量,指向文件信息区,通过它能间接找到与它关联的文件。
文件在读写之前应先打开,使用结束后应关闭。
编写程序时,打开文件的同时会返回一个 FILE*
类型的指针变量,该变量指向该文件,相当于建立了指针和文件的关系。
ANSI C 规定使用 fopen
函数来打开文件,fclose
来关闭文件。
// 打开文件
FILE* fopen ( const char* filename, const char* mode );
// 关闭文件
int fclose ( FILE* stream );
mode
表示文件的打开模式。
常见的文件打开模式如下:
文件打开模式 | 含义 | 若指定的文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 创建一个新文件 |
“a”(追加) | 向一个文本文件尾添加数据 | 创建一个新文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 创建一个新文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 创建一个新文件 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,创建一个新文件 | 创建一个新文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 创建一个新文件 |
“rb+”(读写) | 为了读和写,打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 创建一个新文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读写 | 创建一个新文件 |
#include
int main()
{
FILE* pFile;
pFile = fopen("myfile.txt", "w");
if (pFile != NULL)
{
fputs("fopen example", pFile);
// fputs 函数将字符串 "fopen example" 写入到 pFile 所指向的文件中,且不会自动添加换行符
fclose(pFile);
}
return 0;
}
函数名 | 功能 | 适用于 |
---|---|---|
fgetc | 字符输入函数 | 所有输入流 |
fputc | 字符输出函数 | 所有输出流 |
fgets | 文本行输入函数 | 所有输入流 |
fputs | 文本行输出函数 | 所有输出流 |
fscanf | 格式化输入函数 | 所有输入流 |
fprintf | 格式化输出函数 | 所有输出流 |
fread | 二进制输入 | 所有输入流 |
fwrite | 二进制输出 | 所有输出流 |
上述适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)。
fgetc
和 fputc
#include
int main()
{
FILE *fp1, *fp2;
int ch;
// 打开源文件以读取
fp1 = fopen("source.txt", "r");
if (fp1 == NULL)
{
perror("无法打开源文件");
return 1;
}
// 打开目标文件以写入
fp2 = fopen("destination.txt", "w");
if (fp2 == NULL)
{
perror("无法打开目标文件");
fclose(fp1);
return 1;
}
// 逐字符读取并写入
while ((ch = fgetc(fp1)) != EOF)
// fgetc(指向用于读取的文件的文件指针)
{
fputc(ch, fp2); // fputc 函数参数依次为 要写入文件的字符、文件指针
}
// 关闭文件
fclose(fp1);
fclose(fp2);
return 0;
}
fgets
和 fputs
#include
#define MAX_LENGTH 100
// 定义一个常量 MAX_LENGTH,用于指定字符数组 line 的最大长度
int main()
{
FILE *fp1, *fp2;
char line[MAX_LENGTH]; // 定义一个字符数组 line,用于存储从源文件中读取的一行文本
// 打开源文件以读取
fp1 = fopen("source.txt", "r");
if (fp1 == NULL)
{
perror("无法打开源文件");
return 1;
}
// 打开目标文件以写入
fp2 = fopen("destination.txt", "w");
if (fp2 == NULL)
{
perror("无法打开目标文件");
fclose(fp1);
return 1;
}
// 逐行读取并写入
while (fgets(line, MAX_LENGTH, fp1) != NULL)
// fgets 函数参数依次为 指向字符数组的指针、要读取的最大字符数、文件指针
// fgets 函数从 fp1 所指向的文件中读取一行文本,最多读取 MAX_LENGTH - 1 个字符,剩下一个位置存储 '\0'
// 读取的文本存储在 line 数组中,并在末尾添加字符串结束符 '\0'
{
fputs(line, fp2); // fputs 函数参数依次为 指向 '\0' 结尾的字符串的指针、文件指针
}
// 关闭文件
fclose(fp1);
fclose(fp2);
return 0;
}
fscanf
和 fprintf
#include
typedef struct
{
int id;
char name[50];
float score;
} Student;
int main()
{
FILE *fp;
Student s = {1, "Alice", 85.5};
// 打开文件以写入
fp = fopen("students.txt", "w");
if (fp == NULL)
{
perror("无法打开文件");
return 1;
}
// 格式化写入
// fprintf 函数的参数依次是文件指针、格式化字符串、要写入的数据
fprintf(fp, "%d %s %.2f", s.id, s.name, s.score);
fclose(fp);
// 打开文件以读取
fp = fopen("students.txt", "r");
if (fp == NULL)
{
perror("无法打开文件");
return 1;
}
// 格式化读取
// 定义一个新的 Student 类型的变量 read_student,用于存储从文件中读取的学生信息
Student read_student;
// fscanf 函数的参数依次是文件指针、格式化字符串、要存储数据的变量的地址
fscanf(fp, "%d %s %f", &read_student.id, read_student.name, &read_student.score);
printf("ID: %d, Name: %s, Score: %.2f\n", read_student.id, read_student.name, read_student.score);
fclose(fp);
return 0;
}
fread
和 fwrite
#include
typedef struct
{
int id;
float value;
} Data;
int main()
{
FILE *fp;
Data data = {1, 3.14};
// 打开文件以二进制写入
fp = fopen("data.bin", "wb");
if (fp == NULL)
{
perror("无法打开文件");
return 1;
}
// 二进制写入
fwrite(&data, sizeof(Data), 1, fp);
// 第一个参数:指向要写入数据的指针,这里是 &data,表示 data 结构体的地址
// 第二个参数:每个数据项的大小,这里使用 sizeof(Data) 来获取 Data 结构体的大小
// 第三个参数:要写入的数据项的数量,这里是 1,表示写入一个 Data 结构体的数据
// 第四个参数:文件指针,指定要写入的文件
fclose(fp);
// 打开文件以二进制读取
fp = fopen("data.bin", "rb");
if (fp == NULL)
{
perror("无法打开文件");
return 1;
}
// 二进制读取
Data read_data;
fread(&read_data, sizeof(Data), 1, fp);
// 第一个参数:指向存储读取数据的缓冲区的指针,这里是 &read_data,表示 read_data 结构体的地址
// 第二个参数:每个数据项的大小,同样使用 sizeof(Data) 获取 Data 结构体的大小
// 第三个参数:要读取的数据项的数量,这里是 1,表示读取一个 Data 结构体的数据
// 第四个参数:文件指针,指定要读取的文件
printf("ID: %d, Value: %.2f\n", read_data.id, read_data.value);
fclose(fp);
return 0;
}
fgetc 和 fputc |
读取源文件的每个字符,写入到目标文件,直到文件结束。 |
fgets 和 fputs |
读取源文件的每一行文本,写入到目标文件,直到文件结束。 |
fscanf 和 fprintf |
针对结构化数据,上述是结构体。将一个结构体的信息以格式化的文本形式写入文件,然后从该文件中读取格式化的数据并存储到另一个结构体中。 |
fread 和 fwrite |
针对二进制数据,上述是结构体。将一个结构体的二进制数据写入文件,然后再从该文件中读取二进制数据到另一个结构体中。 |
函数 | 功能 | 适用场景 |
---|---|---|
scanf |
从标准输入流(通常是键盘)读取格式化数据,按照指定的格式将输入数据存储到对应的变量中。 | 需要从键盘获取用户输入的格式化数据时 |
fscanf |
从指定的输入流(如文件流)读取格式化数据,按照指定的格式将数据存储到对应的变量中。 | 需要从文件中读取格式化数据时 |
sscanf |
从字符串中读取格式化数据,按照指定的格式将字符串中的数据提取并存储到对应的变量中。 | 需要从一个字符串中解析出特定格式的数据时 |
printf |
将格式化的数据输出到标准输出流(通常是显示器)。 | 用于向用户显示格式化的信息 |
fprintf |
将格式化的数据输出到指定的输出流(如文件流)。 | 需要将数据以特定格式写入文件时 |
sprintf |
将格式化的数据输出到字符串中,而不是输出到屏幕或文件。 | 需要将数据格式化为字符串以便后续处理时 |
#include
int main()
{
int age = 25;
float height = 1.75;
char name[] = "Alice";
// 用于存储格式化后字符串的数组
char buffer[100];
// sprintf 函数的参数依次为 指向字符数组的指针、指定了输出格式的字符串(包含普通字符和格式说明符)、要输出的数据
sprintf(buffer, "Name: %s, Age: %d, Height: %.2f", name, age, height);
printf("格式化后的字符串: %s\n", buffer);
// 定义变量用于存储从字符串中解析出的数据
int parsed_age;
float parsed_height;
char parsed_name[20];
// sscanf 函数的参数依次为 指向要从中读取数据的字符串的指针、指定了读取格式的字符串(包含普通字符和格式说明符)、要读取的数据
sscanf(buffer, "Name: %19s, Age: %d, Height: %f", parsed_name, &parsed_age, &parsed_height);
printf("解析后的信息:\n");
printf("姓名: %s\n", parsed_name);
printf("年龄: %d\n", parsed_age);
printf("身高: %.2f\n", parsed_height);
return 0;
}
fseek
fseek
函数根据文件指针的当前位置和指定的偏移量,重新定位文件指针的位置,就好像在文本编辑器中移动光标到指定的位置一样。
#include
int main ()
{
FILE * pFile;
pFile = fopen ( "example.txt" , "wb" );
fputs ( "This is an apple." , pFile );
fseek ( pFile , 9 , SEEK_SET );
// fseek 函数的参数依次为 文件指针、偏移量、偏移量的起始位置
fputs ( " sam" , pFile );
fclose ( pFile );
return 0;
}
ftell
ftell
函数获取当前文件指针相对于文件起始位置的偏移量。
通过调用 ftell
函数,可以知道当前文件指针在文件中的具体位置。
使用场景:
fseek
函数),然后调用 ftell
函数,返回的偏移量就是文件的大小(以字节为单位)。ftell
记录位置,使用 fseek
恢复位置。#include
int main ()
{
FILE * pFile;
long size;
pFile = fopen ("myfile.txt","rb");
if (pFile==NULL)
perror ("Error opening file");
else
{
fseek (pFile, 0, SEEK_END);
size = ftell (pFile); // ftell(文件指针)
fclose (pFile);
printf ("Size of myfile.txt: %ld bytes.\n",size);
}
return 0;
}
rewind
调用 rewind
函数会将文件指针位置重置到文件的起始处。这意味着之后再对该文件进行读写操作时,将从文件的开头开始。
使用场景:
rewind
函数将文件指针重置到起始位置,以便重新开始读取。rewind
函数将文件指针移到开头,再进行相应操作。#include
int main ()
{
int n;
FILE * pFile;
char buffer [27];
pFile = fopen ("myfile.txt","w+");
// 使用 for 循环将大写字母 A 到 Z 写入文件
for ( n='A' ; n<='Z' ; n++)
fputc ( n, pFile);
rewind (pFile); // rewind (文件指针)
fread (buffer,1,26,pFile); // 文件指针已重置到文件的起始位置,读取文件的全部内容
fclose (pFile);
buffer[26]='\0';
printf(buffer);
return 0;
}
feof
函数用于在文件读取结束后,判断结束原因是否为遇到文件尾。在文件读取时,不能直接用 feof
函数返回值判断文件是否结束。
fgetc
时看返回值是否为 EOF
,用 fgets
时看返回值是否为 NULL
。#include
#include // 这里主要因为使用 EXIT_FAILURE 这个宏
// 它通常被定义为一个非零值,用于表示程序异常退出的状态码
int main(void) // void 表示该函数不接受任何参数
{
int c; // 注意:int,非 char
// fgetc 读取失败或遇到文件结束时,都会返回 EOF
// EOF 是一个宏,其值通常为 -1,char 类型无法表示 -1,而 int 类型可以
FILE* fp = fopen("test.txt", "r");
if(!fp)
{
perror("File opening failed");
return EXIT_FAILURE;
}
while ((c = fgetc(fp)) != EOF)
// 此为标准 C I/O 读取文件的循环
// fgetc 读取失败或遇到文件结束时,都会返回 EOF
// 读取的字符存于变量 c,若未到文件末尾(即 c 不等于 EOF),则循环继续
{
putchar(c);
}
// 判断文件读取结束是因为正常到达文件末尾,还是因为读取过程中发生 I/O(输入/输出)错误
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
}
ferror
函数主要用于检查文件流是否发生了 I/O 错误。若 ferror 函数返回非零值,说明文件流在读取过程中发生了 I/O 错误。perror
函数用于输出系统调用的错误信息,结合errno
打印具体的错误描述。perror( NULL 或空字符串 ); // 仅打印错误信息
perror("File opening failed"); // 或者 perror("打开文件失败"); // 若文件打开失败,输出 双引号中内容:具体错误描述
- 两者可结合使用,在文件操作中先使用
ferror
检查文件流错误,若系统调用失败则使用perror
输出详细错误信息。
#include
enum { SIZE = 5 }; // 定义一个枚举常量 SIZE
int main(void)
{
double a[SIZE] = {1.,2.,3.,4.,5.};
FILE *fp = fopen("test.bin", "wb");
if (fp == NULL)
{
perror("Failed to open file for writing");
return 1;
}
fwrite(a, sizeof *a, SIZE, fp);
fclose(fp);
double b[SIZE];
fp = fopen("test.bin","rb");
if (fp == NULL)
{
perror("Failed to open file for reading");
return 1;
}
size_t ret_code = fread(b, sizeof *b, SIZE, fp);
if(ret_code == SIZE)
{
puts("Array read successfully, contents: ");
for(int n = 0; n < SIZE; ++n)
printf("%f ", b[n]);
putchar('\n');
}
else
{
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp))
{
perror("Error reading test.bin");
}
}
fclose(fp);
}
ANSI C 标准使用“缓冲文件系统”处理数据文件。系统会自动在内存为每个正在使用的文件开辟“文件缓冲区”。
从内存向磁盘(文件)输出数据时,先存入缓冲区,满后再一起写入磁盘;
从磁盘向计算机读入数据时,先将数据读入缓冲区,填满后,再逐个送至程序数据区(如程序变量)。
缓冲区大小由 C 编译系统决定。
因为有缓冲区的存在,C 语言在操作文件时,需要刷新缓冲区或者在文件操作结束时关闭文件,否则可能导致读写文件的问题。
手动刷新缓冲区:使用fflush
函数强制将缓冲区数据写入文件。
自动刷新缓冲区:调用fclose
函数关闭文件时,会自动刷新缓冲区。
// VS2022 ,Windows 11 环境测试
#include
#include // 这里主要因为使用 Sleep 函数
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf); // 先将代码放在输出缓冲区
printf("睡眠 10 秒 - 已经写数据了,打开 test.txt 文件,发现文件没有内容\n"); // 由于数据还在缓冲区,文件中不会有内容
Sleep(10000); // 程序暂停执行 10000 毫秒(即 10 秒),给用户时间打开 test.txt 文件查看
printf("刷新缓冲区\n");
fflush(pf); // 刷新缓冲区时,才将输出缓冲区的数据写到文件
// 注:fflush 在高版本的 VS 上不能使用了
printf("再睡眠 10 秒 - 此时,再次打开 test.txt 文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
pf = NULL;
return 0;
}