目录
- 预备知识:fopen和fclose
- 1.字符读写:fputc和fgetc
- 2.文本行读写:fputs和fgets
- 3.格式化读写:fprintf和fscanf
- 4.二进制读写:fwrite和fread
- 5.格局打开
预备知识:fopen和fclose
如果我们要读写一个文件,就必须先打开这个文件,读写完后,还需要关闭这个文件。这就像,你要喝一杯水,需要先打开杯盖,才能喝水,喝完水后还需要把盖子盖上。
打开文件的原理是,打开文件后,在内存中创建一个FILE类型的变量,用来记录打开的文件的相关信息。FILE类型是一个结构体类型。
关闭文件的原理是,根据这个FILE类型的变量里描述的文件信息,通过一定手段把文件关闭。
在学习C语言的过程中,我们不需要知道具体的细节,会用就行了。C语言中打开文件需要使用函数fopen,关闭文件需要使用函数fclose。
fopen的声明如下:
FILE * fopen ( const char * filename, const char * mode );
看这个声明,可以了解到,第一个参数就是要打开文件的文件名,第二个参数是用什么方式打开(读?写?还是其他模式?)。函数会打开这个文件,在内存中创建一个相对应的文件信息区,其实就是创建一个FILE类型的变量,这个变量记录了文件的相关信息。接着,这个函数会返回这个FILE变量的地址,如果函数打开文件失败会返回NULL指针。
这里为了简单起见,都在工程目录下操作文件,所以文件名不用带上路径。如果要在其他位置操作文件,根据具体情况带上绝对路径或者相对路径就可以了。
假设我们要操作的文件的文件名是test.txt,我们想要写这个文件(写文件的模式是"w",及write的简写),可以这么调用这个函数:
FILE* pf = fopen("test.txt", "w");
这里的2个参数都用双引号引起,因为是字符串。返回值需要使用一个FILE*的指针来接收。和malloc类似,需要检查返回值是否为NULL指针,如果为NULL指针,则打开文件失败,需要进行错误处理,举个例子:
if (pf == NULL)
{
perror("fopen");
exit();
}
以上的代码中,当检查到pf为NULL,此时打开文件失败,用perror报个错,再exit掉,终止进程。
当然,操作文件不只有"w"一种模式。本篇博客主要介绍比较常见的4种模式,分别是:
- “w” - 写文件,即write的简写。
- “r” - 读文件,即read的简写。
- “wb” - 通过二进制的方式写文件,b是binary的缩写。
- “rb” - 通过二进制的方式读文件。
fclose函数的声明如下:
int fclose ( FILE * stream );
具体的使用很简单,前面我们用一个FILE*的指针来接收fopen函数的返回值,只需要把这个指针传给fclose就能关闭对应的文件了。和free函数类似,fclose函数没有能力把传给它的指针置为NULL,为了防止野指针,需要程序员手动置为NULL值。
fclose(pf);
pf = NULL;
1.字符读写:fputc和fgetc
fputc用于向文件中写入一个字符。
读写文件前,应该打开文件。这次以"w"的模式打开。
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
exit();
}
接下来是使用fputc写文件的操作。写完文件后,需要关闭文件。
fclose(pf);
pf = NULL;
后面的函数都是按照打开文件->读写文件->关闭文件的顺序,唯一的区别是打开文件的方式不一样,也就是fopen的第二个参数不一样。
fputc的声明如下:
int fputc ( int character, FILE * stream );
很明显,第一个参数表示写入的字符,第二个参数表示指向文件信息区的文件指针。比如,如果我要把字符’a’写到pf对应的文件里,应该这么写:
fputc('a', pf);
举一反三:如果要把小写的a~z,总共26个字母写到文件中,应该如何写呢?
for (int ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
程序执行的结果是:创建了一个test.txt文件,并写入了a~z。
接下来,我们来读一下这个文件。使用fgetc之前也需要打开文件,打开的模式是"r",使用完后需要关闭文件。
fgetc是用来读取一个字符的。声明如下:
int fgetc ( FILE * stream );
函数会读一个字符然后返回这个字符的ASCII码值。如果读取失败会返回EOF。
如果想读取刚刚写完的文件,把26个字母读出来,可以使用循环读26次,但是如果不知道文件中有多少字符呢?那就一直读取,直到读完为止。那如何判断读取结束呢?当fgetc返回EOF的时候读取就结束了。
int ch =;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);
}
运行结果如下:
2.文本行读写:fputs和fgets
使用fputs之前,要以"w"的模式打开文件,使用完后要关闭文件,操作同上。
fputs是用来写入文本行的,声明如下:
int fputs ( const char * str, FILE * stream );
str表示要写入的字符串。比如,我写10行Hello, world!!!进去,每写一个就换个行。
for (int i =; i < 10; i++)
{
fputs("Hello, world!!!\n", pf);
}
可以发现,test.txt中就有了10行Hello, world!!!。
想把它们读出来,可以使用fgets,打开文件的模式是"r"。声明如下:
char * fgets ( char * str, int num, FILE * stream );
str表示你想把读到的字符串存在哪,num表示存储的空间有多大(最多存多少个字符,包括字符串结尾的’\0’)。函数会把str返回回来,如果读取失败,会返回NULL指针。所以读取时,可以使用循环,通过每次判断返回值是否为NULL指针,来判断是否读取结束。
char str[] = {0};
while (fgets(str,, pf))
{
puts(str);
}
输出结果:
由于写入时每个Hello, world!!!后面都写了个’\n’,而puts会在打印完字符串后也换个行,所以相当于每次打印完Hello, world!!!后面都换2次行。如果你只想换一次行,可以使用printf来实现:
char str[] = { 0 };
while (fgets(str,, pf))
{
printf("%s", str);
}
输出结果如下:
3.格式化读写:fprintf和fscanf
fprintf和fscanf,与printf和scanf非常像,唯一的区别就是,fprintf和fscanf前面多了个f。(emmm,听君一席话,如听一席话)
在学习这两个函数之前,建议先学一下sprintf和sscanf这两个函数,点这里
我还是采取同样的讲解思路。你也许不知道fprintf和sscanf,但你一定知道printf和scanf(别告诉我你不知道)。
先说printf。举个例子,假设有一个结构体:
struct S
{
int i;
double d;
char arr[];
};
我创建了一个结构体变量:
struct S s = {, 3.14, "abcdef"};
我想你把这个结构体的数据用printf打印到屏幕上,你会怎么写?
printf("%d %lf %s\n", s.i, s.d, s.arr);
如果这些数据不是打印到屏幕上,而是“打印”到文件中,只需要在函数名前面加个f,所有参数最前面加个pf,就行了。
fprintf(pf, "%d %lf %s\n", s.i, s.d, s.arr);
简单吧?注意fprintf需要的打开文件方式是"w",使用完后需要关闭文件。来看看此时的test.txt文件:
干得漂亮!接下来来看看fscanf。fscanf使用前需要使用"r"模式打开文件,使用完后需要关闭文件,都学到这了,别忘了!(稍稍总结一下,目前所有写文件操作打开文件的模式都是"w",即write的简写,而读文件操作打开文件的模式都是"r",即read的缩写。)
还是那句话,你也许没有听说过fscanf,但你一定直到scanf。假设我创建了一个结构体变量:
struct S s = {};
我想你用scanf函数,实现从键盘中输入数据到s中,你会怎么写?
scanf("%d %lf %s", &s.i, &s.d, s.arr);
如果不是从键盘中输入数据,而是从文件中读取数据,只需要在函数名前加个f,参数最前面加个pf就行了。
fscanf(pf, "%d %lf %s", &s.i, &s.d, s.arr);
读完之后,我们可以把s中的数据打印出来。
printf("%d %lf %s\n", s.i, s.d, s.arr);
看吧,读取成功了。
4.二进制读写:fwrite和fread
前面我们都是读写字符文件,也就是以字符的形式读写文件,这是我们能看的懂的形式。但是在计算机的世界中,都是二进制的,所以我们还需要学习用二进制的方式来读写文件。
先来学习下fwrite。由于是二进制的形式读写文件,打开文件的模式是"wb",b代表二进制。函数的声明如下:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
fwrite函数和fread函数的参数列表是一样的,所以学会一个就相当于两个都学会了。第一个参数表示你要写入的数据在内存中存储的位置,第二个参数表示写入一个数据的大小,第三个参数表示要写入几个数据。
比如,我有一个结构体变量:
struct S s = {, 3.14, "abcdef"};
我想把它以二进制的形式写到内存中,第一个参数就是s的地址,第二个参数就是一个结构体的大小,由于我只想写1个s进去,所以第三个参数就是1。
fwrite(&s, sizeof(s),, pf);
输出结果:
看不懂呀!很正常,因为这玩意是二进制。接下来我们用二进制的方式来把数据重新读出来。
fread是用来二进制的读文件的,打开文件的模式是"rb",使用完后需要关闭文件。函数的声明如下:
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
第一个参数表示要把读到的数据存到哪,第二个参数表示读取一个数据的大小,第三个参数表示要读几个数据。
比如,我们先创建一个结构体:
struct S s = {};
接着把读到的数据放到s里,有了前面fwrite的经验,写起来就简单了。
fread(&s, sizeof(s),, pf);
接着把s中的数据打印出来:
printf("%d %lf %s\n", s.i, s.d, s.arr);
输出结果如下:
5.格局打开
其实,我们可以把“屏幕”和“键盘”也当成文件。用stdout表示屏幕,stdin表示键盘,举个例子:
fprintf(stdout, "%d %lf %s\n", s.i, s.d, s.arr);
这行代码会把结构体s中的数据写入到stdout这个“文件”中,而stdout表示屏幕,所以也就把结构体中的数据打印到屏幕上了!哈哈哈,有意思吧。
其实,stdout是标准输出流,而stdin是标准输入流。在前面的函数中,只要是以"w"模式打开文件才能使用的函数(fputc, fputs, fprintf)的FILE*类型的参数就可以传stdout,而以"r"模式打开文件才能使用的函数(fgetc, fgets, fscanf)的FILE*类型的参数就可以传stdin,分别表示“写屏幕”(把数据打印到屏幕上)和“读键盘”(从键盘中输入数据)。
你可能纳闷了,为啥stdout和stdin就不用有fopen和fclose这样的操作呢?这是因为,只要一个C程序跑起来,就默认打开了三个流,分别是:stdout(标准输出流),stdin(标准输入流)和stderr(标准错误流),所以不需要我们手动打开。
这时,你再想想,printf和fprintf有什么区别?区别就是,printf只能格式化输出标准输出流的数据,而fprintf可以格式化输出任意输出流的数据(包括标准输出流和文件流);同理,scanf和fscanf的区别是,scanf只能格式化输入标准输入流中的数据,而fscanf可以输入任意输入流的数据(包括标准输入流和文件流)。那sprintf和sscanf和它们之间有什么区别呢?这两个函数是用来进行格式化数据和字符串的相互转换的,就跟这些“流”没什么关系了。