在C语言编程中,我们编写的程序数据通常存储在电脑内存里。可一旦程序退出,内存就会回收这些数据,再次运行程序时,之前的数据就找不到了。为了能长久保存数据,我们就需要用到文件。文件可以把数据存储在磁盘上,即使程序关闭,数据依然存在,下次运行程序时还能读取使用。
在程序设计里,文件分为程序文件和数据文件。程序文件包括源程序文件(后缀为.c)、目标文件(Windows环境后缀为.obj)和可执行程序(Windows环境后缀为.exe) 。而数据文件,简单来说,就是程序运行时读写的数据所在的文件,它的内容不一定是程序代码。比如,一个记录用户信息的文件,或者程序运行过程中生成的日志文件等。
每个文件都要有一个独一无二的标识,方便我们识别和引用,这就是文件名。文件名由文件路径 + 文件名主干 + 文件后缀组成,像 “c:\code\test.txt” 就是一个完整的文件名。
根据数据的组织形式,数据文件分为文本文件和二进制文件。数据在内存中是以二进制形式存储的,如果直接把数据不加转换地输出到外存文件中,这个文件就是二进制文件。而如果要求数据在外存上以ASCII码的形式存储,那在存储前就需要进行转换,以这种形式存储的文件就是文本文件。
在C程序里,数据的输入输出操作涉及到各种外部设备,为了方便程序员操作,就引入了“流”的概念。程序的数据通过流来与外部设备进行交互。比如,从键盘输入数据,向屏幕输出数据,以及对文件的读写操作,都是通过流来实现的。
C语言程序启动时,会默认打开3个流:
stdin - 标准输入流,大多数情况下从键盘输入数据,scanf 函数就是从标准输入流中读取数据的。stdout - 标准输出流,多数环境下输出到显示器界面,printf 函数就是将信息输出到标准输出流。stderr - 标准错误流,通常也输出到显示器界面 。这三个流的类型都是 FILE*,也就是文件指针,C语言通过文件指针来管理流的各种操作。
在缓冲文件系统中,文件指针非常重要。每个被使用的文件在内存里都会开辟一个文件信息区,用来存放文件的相关信息,像文件名、文件状态以及文件当前的位置等。这些信息保存在一个结构体变量中,这个结构体由系统声明,叫做 FILE。不同的C编译器中,FILE 类型包含的内容会有些差异,但大致相似。
当我们打开一个文件时,系统会自动创建一个 FILE 结构的变量,并填充好相关信息,我们不用关心具体细节,只需要通过一个 FILE 指针来操作这个文件就可以了。比如:
FILE* pf; 这里定义了一个文件指针变量 pf,它可以指向某个文件的文件信息区,通过这个指针就能访问对应的文件。
在对文件进行读写操作之前,要先打开文件,使用完后要关闭文件。ANSI C规定用 fopen 函数打开文件,fclose 函数关闭文件。fopen 函数的原型是:
FILE * fopen ( const char * filename, const char * mode );其中,filename 是要打开的文件名,mode 是文件的打开模式。fclose 函数的原型是:
int fclose ( FILE * stream );stream 就是 fopen 函数返回的文件指针。
文件的打开模式有很多种,常见的如下:
模式大全表:
模式 | 说明 | 文件存在 | 文件不存在 |
|---|---|---|---|
r | 只读(文本) | 打开 | 失败 |
w | 新建写入(清空内容) | 清空 | 新建 |
a | 追加写入 | 追加 | 新建 |
r+ | 读写(从开头) | 打开 | 失败 |
w+ | 新建读写(清空内容) | 清空 | 新建 |
a+ | 读写(追加写入) | 打开 | 新建 |
rb | 二进制只读 | 打开 | 失败 |
wb | 二进制写入 | 清空 | 新建 |
下面是一个打开和关闭文件的示例代码:
#include <stdio.h>
int main ()
{
FILE * pFile;
pFile = fopen ("myfile.txt","w");
if (pFile!=NULL)
{
fputs ("test",pFile);
fclose (pFile);
}
return 0;
}在这段代码中,我们以 “只写” 模式打开了 myfile.txt 文件,向文件中写入了 “test”,最后关闭了文件。
文件的顺序读写是指按照文件内容的先后顺序进行读写操作。C语言提供了一系列函数来实现文件的顺序读写(输入流即为所有输入流):
fgetc:字符输入函数,用于从输入流中读取一个字符。fputc:字符输出函数,向输出流中写入一个字符。fgets:文本行输入函数,从输入流中读取一行文本。fputs:文本行输出函数,向输出流中写入一行文本。fscanf:格式化输入函数,按照指定格式从输入流中读取数据。fprintf:格式化输出函数,按照指定格式向输出流中写入数据。fread:二进制输入函数,用于从文件中读取二进制数据。fwrite:二进制输出函数,向文件中写入二进制数据。下面是一个使用 fputc 和 fgetc 函数进行文件读写的示例:
#include <stdio.h>
int main()
{
FILE *fp1, *fp2;
char ch;
// 以只写模式打开文件1
fp1 = fopen("file1.txt", "w");
if (fp1 == NULL)
{
printf("无法打开文件1\n");
return 1;
}
// 向文件1中写入字符
fputc('A', fp1);
fputc('B', fp1);
fputc('C', fp1);
fclose(fp1);
// 以只读模式打开文件1
fp2 = fopen("file1.txt", "r");
if (fp2 == NULL)
{
printf("无法打开文件1\n");
return 1;
}
// 从文件1中读取字符并输出
ch = fgetc(fp2);
while (ch != EOF)
{
printf("%c ", ch);
ch = fgetc(fp2);
}
fclose(fp2);
return 0;
}在这个示例中,我们先以只写模式打开 file1.txt 文件,向里面写入了 A、B、C 三个字符,然后关闭文件。接着又以只读模式打开这个文件,使用 fgetc 函数逐个读取字符并输出到屏幕上。
有时候,我们需要在文件中随机地读写数据,而不是按顺序读写。C语言提供了几个函数来实现文件的随机读写:
fseek:根据文件指针的位置和偏移量来定位文件指针。函数原型为 int fseek ( FILE * stream, long int offset, int origin );,stream 是文件指针,offset 是偏移量,origin 是起始位置,取值可以是 SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件末尾)。ftell:返回文件指针相对于起始位置的偏移量,函数原型为 long int ftell ( FILE * stream );。rewind:让文件指针的位置回到文件的起始位置,函数原型为 void rewind ( FILE * stream );。下面是一个使用 fseek 函数的示例:
#include <stdio.h>
int main ()
{
FILE * pFile;
pFile = fopen ( "example.txt" , "wb" );
fputs ( "This is an apple." , pFile );
fseek ( pFile , 9 , SEEK_SET );
fputs ( " sam" , pFile );
fclose ( pFile );
return 0;
}在这段代码中,我们先向 example.txt 文件中写入了 “This is an apple.”,然后使用 fseek 函数将文件指针移动到第9个字符的位置(origin + offset),接着再写入 “ sam”。这样,文件的内容就变成了 “This is a sampple.” 。
feof() 是C标准库中专门用于检测文件结束的函数。它的原型如下:
int feof(FILE *stream);注意:feof() 只有在尝试读取超出文件末尾的数据后才会返回 true。(也就是说,遇到错误停止时,如果没到末尾,也返回false,这就会导致实际上已经结束了,但是feof() 认为没有结束)因此,不能直接用 feof 函数的返回值来判断文件是否结束,通常需要结合其他函数使用。
对于文本文件,我们可以通过判断返回值是否为 EOF(fgetc 函数)或者 NULL(fgets 函数)来确定是否读取结束。例如:
#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
printf("文件打开失败!\n");
return 1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) { // 读取字符
putchar(ch); // 输出字符
}
if (feof(fp)) {
printf("\n已到达文件末尾\n");
} else {
printf("\n读取过程中发生错误\n");
}
fclose(fp);
return 0;
}#include <stdio.h>
int main() {
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
printf("文件打开失败!\n");
return 1;
}
char buffer[100];
while (fgets(buffer, 100, fp) != NULL) { // 读取一行
printf("%s", buffer);
}
if (feof(fp)) {
printf("\n已到达文件末尾\n");
} else {
printf("\n读取过程中发生错误\n");
}
fclose(fp);
return 0;
}fread() 函数返回实际读取的数据项数量。如果返回值小于请求的数量,则可能到达文件末尾或发生错误。
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);示例:
#include <stdio.h>
int main() {
FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) {
printf("文件打开失败!\n");
return 1;
}
int buffer[10];
size_t itemsRead;
// read()返回值 < 0 : 到达末尾 or 错误
while ((itemsRead = fread(buffer, sizeof(int), 10, fp)) > 0) {
for (size_t i = 0; i < itemsRead; i++) {
printf("%d ", buffer[i]);
}
}
if (feof(fp)) {
printf("\n已到达文件末尾\n");
} else {
printf("\n读取过程中发生错误\n");
}
fclose(fp);
return 0;
}ferror() 函数:if (ferror(fp)) {
printf("读取过程中发生错误\n");
}ANSI C标准采用“缓冲文件系统”来处理数据文件。在这种系统下,系统会自动在内存中为每个正在使用的文件开辟一块“文件缓冲区” 。
从内存向磁盘输出数据时,数据会先被送到内存中的缓冲区,等缓冲区装满后,才会一起被送到磁盘上。从磁盘向计算机读入数据时,先从磁盘文件中读取数据到内存缓冲区,装满缓冲区后,再逐个将数据送到程序数据区。缓冲区的大小由C编译系统决定。
下面的代码展示了文件缓冲区的作用:
#include <stdio.h>
#include <windows.h>
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
return 0;
}在这段代码中,我们向文件写入 “abcdef” 后,文件中并不会立即出现这些内容,因为数据先存放在缓冲区中。当我们调用 fflush 函数刷新缓冲区或者调用 fclose 函数(自动刷新缓冲区)关闭文件时,缓冲区的数据才会真正写入到文件中。