系列文章目录
目录
前言
在使用电脑写程序时,你知道数据是储存在哪的吗?我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失 了,等再次运⾏程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用文件。
一、文件介绍
磁盘上的文件是文件。 但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度度来分类的)。
1.1 程序文件
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows 环境后缀为.exe)。
1.2 数据文件
文件的内容不⼀定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输⼊数据,运行结果显示到显⽰器上。 其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使⽤,这里处理的就是磁盘上⽂件。我们重点介绍数据文件。
1.3 文件名
⼀个文件要有⼀个唯⼀的文件标识,以便用户识别和引⽤。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名。
二、二进制文件和文本文件
根据数据的组织形式,数据⽂件被称为文本文件或者二进制文件。 数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的⽂件就是文本文件。
⼀个数据在内存中是怎么存储的呢?
字符⼀律以ASCII形式存储,数值型数据既可以⽤ASCII形式存储,也可以使⽤⼆进制形式存储。 如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占⽤5个字节(每个字符⼀个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2019测试)。
三、打开文件和关闭文件
3.1 流的概念
针对文件、画面、键盘等的数据的输入输出操作,都是通过流进行的。我们可以想象成流淌着字符的河。我们所用到的printf函数或者scanf函数都用到了流。
下图printf函数将字符A、B、C输出到连接显示器的流而从键盘输入的字符会进入流中,scanf 函数会将它们取出来,并将它们的值保存至变量x。
3.1.1 标准流
我们之所以能够如此简单方便地执行使用了流的输入输出操作,是因为c语言程序在启动时已经将标准流准备好了。
标准流有三种。
- stdin--标准输出流
在大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
- stdout--标准输入流
大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出流中。
- stderr--标准错误流
用于写出的错误流,大多数环境中输出到显⽰器界面。
这是默认打开了这三个流,我们使⽤scanf、printf等函数就可以直接进行输⼊输出操作的。
stdin、stdout、stderr 三个流的类型是: FILE* ,通常称为文件指针。
C语⾔中,就是通过 FILE* 的⽂件指针来维护流的各种操作的。
3.2 文件指针FILE*
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。 每个被使用的文件都在内存中开辟了⼀个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及⽂件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系统声明的,取名FILE。
例如,VS2013编译环境提供的 stdio.h 头文件中有以下的文件类型申明:
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
struct _iobuf { char *_ptr; int _cnt; char *_base; int _flag; int _file; int _charbuf; int _bufsiz; char *_tmpfname; }; typedef struct _iobuf FILE;
每当打开一个文件的时候,系统会根据文件的情况自动创建⼀个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使⽤起来更加方便。 下⾯我们可以创建⼀个FILE*的指针变量:
FILE* pf;//⽂件指针变量
定义pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是⼀个结构体变 量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件。
3.3 打开文件和关闭文件
在使用纸质笔记本时通常时先打开,然后再阅读或者在适当的地方书写。程序中的文件处理过程也同样如此。首先打开文件并定位到文件开头,然后找到要读取或写入的目标位置进行读写操作,最后将文件关闭。
打开文件的操作称为open。函数库中的fopen函数用于打开文件。
该函数需要俩个参数
第一个参数是要打开的文件名,第二个参数是文件类型及打开模式。
FILE打开成功返回一个指向对象的指针,打开失败则返回空指针。
关闭文件使用的是fclose();在程序最后记得关闭同时给文件指针的值置为NULL;这样能有效避免野指针。
下面我们使用''r''模式打开文件"abc.txt"。(文件类型有俩种,文本文件和二进制文件,这里我们先讲文本文件)。
#include <stdio.h>
int main()
{
FILE* fp = fopen("abc.txt", "w");//打开文件
if (fp != NULL)
{
fputs("fopen example", fp);
fclose(fp);关闭文件
fp = NULL;//避免野指针的出现
}
return 0;
}
文件使用方式 | 含义 | 如果指定文件不存在 |
“r”(只读) | 为了输⼊数据,打开⼀个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开⼀个文本文件 | 建立⼀个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立⼀个新的文件 |
“rb”(只读) | 为了输⼊数据,打开⼀个⼆进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开⼀个⼆进制文件 | 建立⼀个新的文件 |
“ab”(追加) | 向⼀个⼆进制文件尾添加数据 | 建立⼀个新的文件 |
“r+”(读写) | 为了读和写,打开⼀个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议⼀个新的文件 | 建立⼀个新的文件 |
“a+”(读写) | 打开⼀个文件,在文件尾进行读写 | 建立⼀个新的文件 |
“rb+”(读写) | 为了读和写打开⼀个⼆进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建⼀个新的⼆进制文件 | 建立⼀个新的文件 |
“ab+”(读写) | 打开⼀个⼆进制文件,在文件尾进行读和写 | 建立⼀个新的文件 |
通过具体例子,介绍一下''r'' 、 ''w'' 、 ''rb'' 、 ''wb'' 这四个模式。
''w''模式,如果没有要找的文件,他会在text.c所在的目录下建立一个新的文件
#include <stdio.h>
int main()
{
FILE* fp = fopen("abc.txt", "w");//打开文件
if (fp != NULL)
{
fputs("fopen example", fp);
fclose(fp);关闭文件
fp = NULL;//避免野指针的出现
}
return 0;
}
fopen中的路劲也可以是绝对路径,但是在写入代码是要注意转义,所以将\变成俩个\\,我们所写的是相对路径(相对与text.c( 我们程序的文件目录))
fputs是向fp所指向的文件中输出数据。我们输入的内容就会再文本文件中显示。(如果存在文件abc.txt,并且里面有所保存的内容,会将内容删除,输入代码所包含的数据)。
如果要在text.c所在目录的上一级或者上上一级目录里建立。我们可以通过下面的代码来实现
.表示当前目录 ..表示上一级目录
perror函数是一个打印错误信息的函数,用法简单,一般放在每一个可能出现错误的代码后面,perror()空格里面不写就直接是错误信息,因此加上"fopen"可以明确表示是我们上面的fopen出错了。
int main()
{
//FILE* fp = fopen("./abc.txt", "w");//当前目录写与不写都行
FILE* fp = fopen("./../abc.txt", "w");//上一级目录
//FILE* fp = fopen("./../../abc.txt", "w");//上上一级目录
if (fp == NULL)
{
perror("fopen");
return 1;
}
fputs("fopen example", fp);
fclose(fp);
fp == NULL;
return 0;
}
可见在文件名前加上./../就是在上一级目录建立。改成./../../就是在上上级目录里建立。
''r''模式:
同样的代码,删除''abc.txt''文件之后,因为''r''模式没有找到文件(打开失败),fopen会返回一个空指针,此时fp == NULL。
如果存在该文件,则会读取该文件内容,然后再进行操作。
接下来我们介绍一下顺序读写,再下面介绍''wb'',''rb''.
四、文件的顺序读写
4.1 顺序读写函数的介绍
函数名 | 功能 | 适⽤于 |
fgetc | 字符输⼊函数 | 所有输⼊流 |
fputc | 字符输出函数 | 所有输出流 |
fgets | 文本行输⼊函数 | 所有输⼊流 |
fputs | 文本行输出函数 | 所有输出流 |
fscanf | 格式化输⼊函数 | 所有输⼊流 |
fprintf | 格式化输出函数 | 所有输出流 |
fread | ⼆进制输⼊ | 文件 |
fwrite | ⼆进制输出 | 文件 |
在没了解这些之前,我们使用的printf,scanf都是标准输出,输入流(stdout、stdin),都是在终端(及屏幕上)输入输出。如下图
而我们顺序读写函数(除了fwrite、fread)适用于所有输出、输入流。那么它们也可以在屏幕上显示。
4.1.1 fgetc与fputc
介绍前俩个函数。
fgetc的参数表示输入到哪。fputc的是将第一个参数输出到第二个参数(输出到哪)。俩个读取失败都是返回EOF。fgetc读取失败或者读取到文件末尾就是返回的是EOF.
在向文本文件输入输出时是通过ASCII值来进行的。
#include <stdio.h>
int main()
{
char a = fgetc(stdin);//从屏幕上输入
fputc(a,stdout);//从屏幕上输出
return 0;
}
记下来我在''w''、''r''模式下使用这俩个函数。首先介绍''w''中写文件,fputc。
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fputc('d', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
在改善一下把26个字母全写进去。再加上换行。
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
/*fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fputc('d', pf);*/
int i = 0;
for (i = 0; i < 26; i++)
{
fputc('a' + i, pf);
fputc('\n', pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
当然将for循环里的pf改成stdout则是在屏幕上输出。
接下来介绍读文件,fgetc。当然首先我们文件中得有数据,我们在data.txt文本文件中写入abcd。
//fputc这俩步可以和printf("%c\n",ch);是一个效果
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);//将文件内容输出到屏幕上
fputc(ch, stdout);
fputc('\n', stdout);//fputc这俩步可以和printf("%c\n",ch);是一个效果
ch = fgetc(pf);
fputc(ch, stdout);
fputc('\n', stdout);
ch = fgetc(pf);
fputc(ch, stdout);
fputc('\n', stdout);
ch = fgetc(pf);
fputc(ch, stdout);
fputc('\n', stdout);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
接下来让我们练习一下,练习将一个文件的内容拷贝到另一个文件中
#include <stdio.h>
int main()
{
//打开文件,从中读取文件
FILE* pfread = fopen("data1.txt", "r");
if (pfread == NULL)
{
perror("fopen->data1.txt");
return 1;
}
//打开文件,写入数据。
FILE* pfwrite = fopen("data2.txt", "w");
if (pfwrite == NULL)
{
fclose(pfread);//当写入文件出错,我们应该关掉data1.txt
pfread = NULL;
perror("fopen->data1.txt");
return 1;
}
//数据的读写(拷贝)
//fgetc读取失败或者读取到文件末尾就是返回的是EOF;
int ch = 0;
while ((ch = fgetc(pfread)) != EOF)
{
fputc(ch, pfwrite);
}
//关闭文件
fclose(pfread);
pfread = NULL;
fclose(pfwrite);
pfwrite = NULL;
return 0;
}
运行成功之后。
4.1.2 fgets与fputs
fputs和fputc,前者是输出字符串,后者是输出单个字符。
fgets和fgetc,也是如此。但是fgets要注意它的参数,它有三个参数
第一个和第三个参数是从stream输入到string中。第二个参数是输入多少。
在读文件"r"模式下
在文件"data.txt"中写入abcdefghihehe
测试下面的代码看是否从文件中读取到了内容。fgets就是从文件中输入内容到数组arr中
#include <stdio.h>
int main()
{
// 打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
char arr[20] = "xxxxxxxxxxxxxxxxxxx";
fgets(arr, 10, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行之后,文件的内容确实在arr中。发现它会为'\0'预留一个位置。所以实际上它读取的内容是9个。
同样可以从屏幕上输入。
#include <stdio.h>
int main()
{
// 打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件
char arr[20] = "xxxxxxxxxxxxxxxxxxx";
fgets(arr, 10, stdin);
fputs(arr,stdout);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
效果是同样的。注意'\0'是空白字符,不会显示到屏幕上。
4.1.3 fscanf与fprintf
fprintf与printf非常相似。
fprintf只是多了一个参数。后面的省略号叫做可变参数列表。有兴趣可以了解我们只需要知道怎么用。我们给出下面的代码。
struct Stu
{
char name[20];
int age;
float score;
};
#include <stdio.h>
int main()
{
struct Stu s = { "zhangsan",20,90.5f };
//打开文件
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fprintf(pf,"%s %d %.1f", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
结构体的内容是否会被写入文件中?答案是会。
因此在使用fprintf进行输入的时候,第一个参数就是目的地。后面的参数和printf用法一样即可。
上面的代码fprintf中的pf改成stdout就是在屏幕上输出。
fscanf与scanf也是如此。
同样是fscanf多了一个参数。在"r"模式下来进行fscanf的运用。在data.txt文件中保存结构体的数据
给出下面的代码:
注意fscanf是需要读取地址的,因为结构体中数组名name本身就是地址,而其他的不是因此需要取地址符号。
struct Stu
{
char name[20];
int age;
float score;
};
#include <stdio.h>
int main()
{
struct Stu s = { 0 };
//打开文件
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件//fscanf和scanf一样需要的是地址。
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
fprintf(stdout, "%s %d %.1f", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
输出的是后我们浮点数控制到了小数点后一位%.1f。如果是%f会默认后面有六位小数。
因此fscanf和scanf就只有一个区别。fscanf适用于所有输出流。后面的参数和scanf的使用方法是一样的。fscanf和fprintf可以说包含了scanf和printf的功能。
4.1.4 fread和fwrite
在上面我们使用fprintf-文本的方式向文件内输出了结构体数据。
下面我们来介绍最后俩个函数,使用二进制的方式输入文件data.txt中。
fread | ⼆进制输⼊ | 文件 |
fwrite | ⼆进制输出 | 文件 |
同时我们使用下面俩个读写模式
“rb”(只读) | 为了输⼊数据,打开⼀个⼆进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开⼀个⼆进制文件 | 建立⼀个新的文件 |
fwrite的第一个参数时我们要输出的数据的起始位置。第二个参数是每一个元素的大小单位是字节。第三个参数是一次要写入元素的个数。第四个参数是我们要输出到的文件位置。
struct Stu
{
char name[20];
int age;
float score;
};
#include <stdio.h>
int main()
{
struct Stu s = { "zhangsan",20,90.5f };
//打开文件
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//二进制的形式写文件
//将s中的数据写入pf所指向的文件中,每个元素的大小是sizeof(s)
//写入一个元素。
fwrite(&s, sizeof(s), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
代码输出的结果是
fwrite是以二进制的形式写入。会发现除了zhangsan,后面的数据我们都不能读懂。因为zhangsan的二进制和ASCII码值文本是一一对应的,而整形和浮点型的二进制对应的ASCII码值我们是看不懂的。但是如果是使用fread来输入是可以读取出来的。下面来学习fread
它的参数和fwrite的参数是一样的。fread的第一个参数时我们要输入的数据的起始位置。第二个参数是每一个元素的大小单位是字节。第三个参数是一次要写入元素的个数。第四个参数是我们要输入到的文件位置。
注意不要认为将fread中的第一个参数和最后一个参数换位置就可以实现。要写入二进制数据到文件时必须使用fwrite。
使用fread来读取二进制文件就可以,把文本中二进制的数据读取出来。
struct Stu
{
char name[20];
int age;
float score;
};
#include <stdio.h>
int main()
{
struct Stu s = { 0 };
//打开文件
FILE* pf = fopen("data.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//二进制的形式写文件
fread(&s, sizeof(s), 1, pf);
fprintf(stdout, "%s %d %f", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
以上就是顺序读写的内容。我们要在对应的模式下,用相匹配的函数来进行文件的操作。
如果你看完以上内容有所收获,请留下你宝贵的点赞和关注!谢谢你的观看!