文件操作可以让我们的数据持久化
文件名包括三部分:文件路径+ 文件名主干+文件后缀
例如:文件路径:C:\Users\Public\Desktop
为了方便起见:文件标识就是文件名。
在程序设计中有两种文件:程序文件、数据文件(从文件的功能角度来分类)
程序文件:
包括源程序文件(后缀:.c
)、目标文件(windows环境后缀:.obj
)、可执行文件(windows环境后缀.exe
)
数据文件:
文件的内容不一定是程序,而是程序运行时读取的数据,比如程序原需要从中读取数据的文件,或者输出内容的文件
注意:并不是凡是
.c
后缀的文件就是程序文件,该文件也有可能是数据文件,其他文件也可以从中读取数据或者写入数据。
本篇博客主要是在介绍数据文件
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每一个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态,以及文件当前所处的位置等)。这些信息是保存在一个结构体变量中的,该结构体类型是有具体系统声明的,取名为
FILE
而文件信息区的内容在VS2013版本中有详细的说明(vs2019版本不容易直接看出来):
struct _iobuf{
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
不同的C编译器中FILE类型包含的内容不完全相同。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,我们不用太在意其中的细节。
我们一般都是通过一个FILE类型的指针来维护访问这个FILE结构的变量,这样使用起来更方便
下面我们来创建一个FILE类型的变量
FILE* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量,可以是pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区的信息就可以访问该文件。也就是说:通过文件指针变量可以找到与之关联的文件。
文件在读写之前应该先打开,在使用完后应该关闭文件。在编写程序的时候打开文件的同时就会返回一个FILE类型的指针指向这一个文件,相当于建立了指针和文件的关系
fopen
与fclose
打开文件的函数是fopen
,关闭文件的函数是fclose
下面我们来了解这两个函数:
这是fopen
函数的函数声明
FILE *fopen( const char *filename, const char *mode );
该函数有两个参数:
第一个参数:文件的名字。(文件的路径)
第二个参数:打开的模式,运用模式字符串来控制。
该函数第二个参数(文件打开方式有多种):
模式字符串 | 含义 | 如果没有对应文件 |
---|---|---|
"r"(只读) | 为了输入数据,打开一个已经存在的文本文件 | 打开失败 |
"w"(只写) | 为了输出数据,打开一个文本文件 | 创建一个新的文件 |
"a"(追加) | 向文本文件结尾添加数据 | 创建一个新的文件 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 打开失败 |
“w+”(读写) | 为了读和写,打开一个文本文件 | 创建一个新的文件 |
“a+”(读写) | 打开一个文件,在文件结尾添加数据 | 创建一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个已经存在的二进制文件 | 打开失败 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 创建一个新的文件 |
“ab”(追加) | 向一个二进制文件结尾添加数据 | 创建一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 打开失败 |
“wb+”(读写) | 为了读和写打开一个二进制文件 | 创建一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件结尾添加数据 | 创建一个新的文件 |
这是fclose
函数的函数声明:
int fclose( FILE *stream );
参数是一个文件指针,指向我们打开的文件所对应的文件信息区首地址。
在使用的时候只需要:
fclose(pf)
,就可以关闭对应的文件,但是为了更安全,我们在关闭文件后还需要将该指针赋为空指针:pf = NULL;
下面我们来具体操作一下打开文件和关闭文件:
这里我们使用**
r
与w
**两种不同的模式分别来打开文件:(此时我们的文件目录下没有我们想要访问的文件)
”r“—只读文件:我们知道利用这种方式去打开一个文件,如果文件目录中找不到我们需要打开的文件,就会返回一个空指针:
结果也确实是返回了一个空指针
”w“—只写文件:我们在用”w“模式区打开一个还没有创建的文件:
程序正常进行,没有出现错误,这时候我们再去文件目录下看会发现已经创建了一个新的文件:
文件以某一个顺序进行读写操作。
我们需要知道一些输入输出函数(会详细介绍):
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc |
所有输入流 |
字符输出函数 | fputc |
所有输出流 |
文本行输入函数 | fgets |
所有输入流 |
文本行输出函数 | fputs |
所有输出流 |
格式化输入函数 | fscanf |
所有输入流 |
格式话输出函数 | fprintf |
所有输出流 |
二进制输入函数 | fread |
文件 |
二进制输出函数 | fwrite |
文件 |
任何一个C语言程序只要运行起来就会默认打开三个流
标准输入流---->键盘----
stdin
标准输出流---->屏幕----
stdout
标准错误流---->屏幕----
stderr
fputc
与fgetc
fputc
写文件fputc
是一个字符输出函数,函数声明是:
int fputc( int c, FILE *stream );
该函数的第一个参数是要写入的字符,第二个参数是一个流或者标准输出。
表示会将这个字符写到一个文件或者是标准输出(屏幕)中。
例子1(输出数据到一个文件中):
#include
#include
#include
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写文件
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
程序正常执行
我们也可以在文件目录下找到对应的文件:该文件的内容也确实是我们输入的’a’、‘b’、‘c’。
例子2(输出到标准输出中):
#include
#include
#include
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
fputc('a', stdout);
fputc('b', stdout);
fputc('c', stdout);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们可以看到在屏幕上面打印了字符’a’、‘b’、‘c’。
fgetc
读文件此时我们不能再使用“w”
模式打开文件,我们需要使用满足于读文件的打开方式"r"
;
fgetc
函数的声明:
int fgetc( FILE *stream );
该函数从一个流中读取一个字符,并且返回该字符的
ascll
码值,如果读取失败或者遇见文件结尾会返回EOF该函数的参数就是需要从中读取字符的流。
我们同样用两种情况来解释来运用该函数
1.从文件中读取字符
此时我们的“data.txt”
文件中存放了字符abcdefg
我们利用fgetc
函数将其一次读出:
#include
#include
#include
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);//可以将流中的字符全部读完
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
2.从标准输入中读取字符
#include
#include
#include
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
int ch = 0;
int count = 5;
while (count--)
{
ch = fgetc(stdin);
printf("%c", ch);
}//从键盘输入中读取五个字符
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fgetc
函数成功从标准输入(键盘)中读取了五个字符:
#include
#include
#include
int main()
{
//以读的方式打开第一个文件
FILE* pf1 = fopen("test1.txt", "r");
if (pf1 == NULL)
{
printf("test1.txt: %s", strerror(errno));
return 0;
}
//以写的方式打开的二个文件
FILE* pf2 = fopen("test2.txt", "w");
if (pf2 == NULL)
{
printf("test2.txt: %s", strerror(errno));
fclose(pf1);
pf1 = NULL;
//如果第二个文件打开失败,那么在结束前要关闭前面打开的文件。
return 0;
}
//将第一个文件的内容复制到第二个文件中
int ch = 0;
while ((ch = fgetc(pf1)) != EOF)
{
fputc(ch, pf2);
}
//关闭两个文件
fclose(pf1);
pf1 = NULL;
fclose(pf2);
pf2 = NULL;
return 0;
}
为什么每一次调用该函数的结果不同?
我们看出来每一次调用同一个函数fgetc
,甚至函数里面的参数都是不变的,但是每一次函数调用的结果却有变化。
那是因为文件指针的作用,每一次读取该文件后,文件指针就会向后偏移一位,这样下一次读取的时候,就会读取到下一个字符:
fgets
与fputs
函数前面的一组函数一次只能读写一个字符,而下面这一组函数一次可以对一行字符进行读写
fputs
首先是fputs
函数,它可以直接写一行字符
该函数的声明如下:
int fputs( const char *string, FILE *stream );
该函数的第一个参数是一个字符串,
第二个参数时一个流,可以时文件指针,也可以是标准输出。
利用这个函数向文件中写字符
#include
#include
#include
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写文件
fputs("hello world", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
字符串被成功写入文件:
需要注意:
如果我们在输入的时候没有在\n
,那么在文件中写入字符时,两行字符不会隔开,而是打印在同一行。
同样我们可以利用标准输出将其打印在屏幕上面:
接下来我们利用fgets
读取一行的字符
fgets
声明:
char *fgets( char *string, int n, FILE *stream );
第一个参数是一个指针,可以理解为字符数组,是我们读出的字符需要存放的位置
第二个参数表示最大读取字符的个数,但是要注意:如果参数二值为1000,那么它最大读取字符个数是999,最后一个需要放
\0
;第三个参数是一个流。
我们来利用这个函数去读取文件
#include
#include
#include
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
char buf[1000] = { 0 };
fgets(buf, 1000, pf);
//由于数组buf有1000个元素,所以第二个参数我们设置的最大读取数是1000.
printf("%s", buf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们的文件中一开始保存了“i love beijing”
.
成功读取文件中的字符,并且保存在数组buf
中,然后把数组内存打印出来:
我们也可以利用第二个参数来控制我们需要读取的字符数量
我们将第二个参数设置为10,但是由于需要保证字符串最后一个元素是
\0
,所以只读取了文件中的前九个字符。
该函数的返回值:
如果该函数成功读取字符,那么它会返回保存字符串的数组的首地址,如果该函数遇到错误或者读取到文件末尾,那么它会返回空指针
上面的两组函数进行的是文本的输入输出,而下面这一组函数可以进行格式的输入输出
fprintf
与pscanf
函数fprint
该函数的参数和函数
printf
的参数相似度很高,唯一的区别是:
fprintf
多一个参数,该参数是第一个参数,该参数是一个流
#include
#include
#include
struct Stu {
char name[20];
int age;
double score;
};
int main()
{
struct Stu s = { 0 };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
//写格式化的数据
printf("%s %d %lf\n", s.name, s.age, s.score);
fprintf(pf, "%s %d %lf\n", s.name, s.age, s.score);//输入到文件中
fprintf(stdout,"%s %d %lf\n", s.name, s.age, s.score);//打印到屏幕上
fclose(pf);
pf = NULL;
return 0;
}
我们来看一看效果:
打印的第一遍是printf
函数的效果,打印的第二遍是fprintf
的效果
fscanf
同样我们可以利用fscanf
函数去读文件.
#include
#include
#include
struct Stu {
char name[20];
int age;
double score;
};
int main()
{
struct Stu s = { 0 };
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
//读格式化的数据
fscanf(pf,"%s %d %lf", s.name, &(s.age), &(s.score));
printf("%s %d %lf", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
我们成功的将文件中的数据读取到结构体变量s中,并且把他打印出来:
sprintf
与sscanf
函数:这一组函数不是用来进行文件操作的,但是由于前面介绍了很多输入输出函数,所以这里也将这一组函数也解释一下。:
sprintf
:该函数的原型是:
int sprintf( char *buffer, const char *format [, argument] ... );
该函数的作用是将格式化的数据转换成一个字符串
#include
struct Stu {
char name[20];
int age;
double score;
};
int main()
{
struct Stu s = { "zhangsan",19,61.2 };
char buf[100] = { 0 };
sprintf(buf, "%s %d %lf", s.name, s.age, s.score);
printf("%s", buf);
return 0;
}
sscanf
该函数的原型:
int sscanf( const char *buffer, const char *format [, argument ] ... );
该函数可以把一个字符串中的数据格式化处理
#include
struct Stu {
char name[20];
int age;
double score;
};
int main()
{
struct Stu s = { "zhangsan",19,61.2 };
struct Stu tmp = { 0 };
char buf[100] = { 0 };
//先利用sprintf函数将格式化数据放在buf数组中
sprintf(buf, "%s %d %lf\n", s.name, s.age, s.score);
//打印buf数组
printf("%s", buf);
//再利用sscanf函数将buf数组中的字符串格式化处理放在结构体变量tmp中
sscanf(buf, "%s %d %lf", tmp.name, &(tmp.age), &(tmp.score));
//打印出tmp 的各个成员
printf("%s %d %lf\n", tmp.name, tmp.age, tmp.score);
return 0;
}
fread
与fwrite
函数与前面三组函数不同,这一组函数可以以二进制的方式读与写
fwrite
该函数可以以二进制的方式写,声明是:
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
第一个参数:指针指向的被写的数据
第二个参数:一个元素的大小
第三个参数:最多要写的元素的个数
第四个参数:一个流
我们利用这个函数来把一个数据以二进制的方式写入文件
#include
#include
#include
struct Stu {
char name[20];
int age;
double score;
};
int main()
{
struct Stu s[2] = { {"zhangsan",19,65.1},{"lisi",20,62.1} };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
fwrite(&s, sizeof(struct Stu), 2, pf);
fclose(pf);
pf = NULL;
return 0;
}
这里我们打开记事本发现里面是乱码,因为我们记事本是以文本模式打开文件的,无法解析二进制文件
fread
该函数可以以二进制方式的读文件,函数声明是:
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
从流里面读count个大小为size 的数据放在buffer里面
该函数的返回值是读取到的完整元素的个数
同样,我们运用一次该函数:
#include
#include
#include
struct Stu {
char name[20];
int age;
double score;
};
int main()
{
struct Stu s[2] = { 0 };
FILE* pf = fopen("data.txt", "rb");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
//二进制方式的读文件
fread(s, sizeof(struct Stu), 2, pf);
//打印数组中的内容
printf("%s %d %lf\n", s[0].name, s[0].age, s[0].score);
printf("%s %d %lf\n", s[1].name, s[1].age, s[1].score);
fclose(pf);
pf = NULL;
前面说的都是文件的顺序读写,但是其实文件还可以实现随机读写
我们一般通过使用文件指针来进行文件的随机读写
fseek
int fseek( FILE *stream, long offset, int origin );
根据文件指针的位置和偏移量来定位文件指针
第一个参数是一个流
第二个参数是偏移量,可以向前偏移(负数) ,也可以向后偏移(正数)
第三个参数文件指针的位置,有下面三个选项
选项 | 不同选项所对应的位置 |
---|---|
SEEK_CUR | 文件指针当前的位置 |
SEEK_END | 文件末尾的位置 |
SEEK_SET | 文件起始位置 |
我们知道文件每被读取一次,文件指针就会向后移动一位。
我们来使用一次该函数,此前我们文件中的内容是“abcdef”
;
#include
#include
#include
int main()
{
//打开文件
FILE *pf= fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return;
}
//读文件
int ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);//a
ch = fgetc(pf);
printf("%c\n", ch);//b
//定位文件指针
fseek(pf, 3, SEEK_CUR);
//该操作将文件指针从指向'c'向后偏移三个位置后为指向'f'.
ch = fgetc(pf);
printf("%c\n", ch);//f
fclose(pf);
//关闭文件
pf = NULL;
return 0;
}
ftell
除了刚才的函数以外,我们还需要了解这样一个函数,改函数可以返回文件指针相对于起始位置的偏移量
该函数的声明是:
long ftell( FILE *stream );//该函数唯一的参数就是一个流,然后返回改文件的偏移量
我们把这个函数运用在刚才的代码段:
#include
#include
#include
int main()
{
FILE *pf= fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return;
}
//读文件
int ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//定位文件指针
int offset = ftell(pf);
printf("%d",offset);
fclose(pf);
pf = NULL;
return 0;
}
rewind
我们还有一个函数可以改变文件指针的位置,这个函数是rewind,改函数可以让文件指针回到起始位置
该函数的声明是:
void rewind( FILE *stream );//唯一的参数是一个流
我们将改函数运用在刚才的代码中:
#include
#include
#include
int main()
{
FILE *pf= fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return;
}
//读文件
int ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//让文件指针指向初始位置
rewind(pf);
ch = fgetc(pf);
printf("%c\n",ch);
fclose(pf);
pf = NULL;
return 0;
}
使用了rewind函数后,我们再次读取该文件,然后打印出改字符,结果是打印了第一个字符
根据数据的组织形式,数据文件被称为文本文件和二进制文件。
如果数据在内存中以二进制的形式储存,如果不加转换输出到外存,就是二进制文件。
如果要求在外存上以ASCLL码的形式存储,则需要在储存前转换,以ASCll
码字符的形式存储的文件就是文本文件。
一个数据在内存中是如何储存的?
字符一律是是使用ASCll
形式储存,数值性数据既可以用ASCll
码值存储,也可以用二进制从事存储。
#include
#include
#include
int main()
{
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
int a = 10000;
fwrite(&a, 4, 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
我们以二进制的形式将数据10000存储在文件中,自然不能用文本编辑器查看:文本编辑器不能解析二进制文件,所以我们只能看到乱码
我们将刚才的二进制文件查看可以得到这样的结果:
在读文件的时候,经常会遇到读取文件结束的情况。
feof
我们不能使用该函数的返回值来判定文件是否结束
该函数的真正用法是当我们已经确定文件读取结束的时候,用该函数来判定文件结束的原因:
文本文件读取是否结束,判断返回值是否为EOF,或者 NULL
fgetc
函数读取正常返回改字符的ASCll
码值,读取失败返回EOFfgets
函数读取正常返回改字符串的首地址,读取失败返回空指针二进制文件的读取,应该判断返回值是否小于第三个参数count:
- 如果小于count,那么文件已经结束
- 如果相同,那么至少还因该读取一次才结束
feof
函数的声明:
int feof( FILE *stream );
如果文件读取结束是因为已经读取文件末尾,那么该函数会返回一个非零值
如果文件读取结束时因为其他错误,那么该函数会返回一个0。
#include
#include
#include
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
printf("%s", strerror(errno));
return 0;
}
int ch = 0;
while ((ch = getc(pf)) != EOF)
{
printf("%c\t", ch);
}
if (ferror(pf))//如果在读文件的时候出现错误会执行第一条语句
puts("I/O error when reading");
else if (feof(pf))//如果没有错误则会打印该文件成功达到文件结尾
puts("End of file reached successfully");
fclose(pf);
pf = NULL;
return 0;
}
ANSIC标准采用“缓冲文件系统”处理数据文件的,所谓缓冲文件系统是指系统自动地在内存中位程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输入数据会先送到内存的缓冲区,装满缓冲区后在一起送到磁盘上。如果从磁盘想向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个的将数据送到程序区(程序变量等)。缓冲区的大小根据C编译系统决定。
文件缓冲区在没有充满的时候不会将数据输入或者输出。有点事可以大大提高程序的效率,但是有时候我们的程序却需要尽快的输出或输入数据,这是我们就需要刷新缓冲区。
如果我们刷新缓冲区,那么缓冲区的文件也会直接被读取。
我们可以利用函数fflush
刷新文件缓冲区
这里我们用一个例子来解释文件缓冲区:
#include
#include
#include
#include
int main()
{
FILE* fp = fopen("test.txt", "w");
if (fp == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
fputs("hello world", fp);
printf("睡眠十秒后刷新缓冲区,此时文件中没有信息\n");
Sleep(10000);
fflush(fp);
printf("睡眠十秒后关闭文件,此时文件中已经有信息了\n");
Sleep(10000);
fclose(fp);
fp = NULL;
return 0;
}
注意:在执行关闭文件操作时,会先刷新缓冲区后再刷新文件。
每一步文件操作都和文件指针密切相关,所以我们需要时刻了解文件指针的状态(值得注意:每一次打开文件都需要排除文锦指针为NULL的情况)。文件的读写操作有很多实现方法,文件的读写顺序也分为顺序读写和随机读写,进行顺序读写的函数有很多,他们有各自的特点:有的函数是进行的文本模式操作,有的则是以二进制的形式在操作文件……。文件的随机读写则更需要了解指针的位置。文件读取结束的判定也有值得注意的地方。