前言:
如果没有文件,我们写的程序的数据都是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失了,等程序再次运行,我们是无法看到上次程序的数据的。如果想要将数据持久化的保存,我们可以使用文件来存储数据。
文件指的就是磁盘(硬盘)上的文件
在程序设计中,文件一般有两种,分别是:程序文件、数据文件。
程序文件:
程序文件包括源程序文件(后缀为.c),目标文件(在windows环境下后缀是 .abj),可执行程序(windows环境下后缀是 .exe)
数据文件:
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本篇内容主要学习数据文件相关的内容
在之前,写代码所处理的数据的输入输出都是以终端为对象的,即从终端的键盘上读取数据,运行结果输出(显示)到显示器上。
但是有时,我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件。
文件名:
每一个文件都要有一个唯一的文件标识,以便用户识别和引用
一个文件的文件名包含三部分:文件路径+文件名主干+文件后缀
就比如 text.txt这样一个文本文件,在电脑D盘C语言中study.c这个文件路径中
D: \ C语言 \ study.c \ text.txt
为了方便,文件标识常被成为文件名。
二进制文件和文本文件:
根据数据的组织形式,数据文件又被称为文本文件或者二进制文件
二进制文件:数据在内存中以二进制的形式存储,不加以转换就输出到外存的文件中,就是二进制文件
文本文件:如果要求在外存上以ASCII码的形式存储,就需要在存储之前转换。以ASCII字符的形式存储的文件就是文本文件
那么,一个数据在 文件中怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以用二进制形式存储。
就比如,一个整数10000,以ASCII码的形式输出到磁盘,则磁盘中占5个字节(每一个字符一个字节)而如果用二进制形式输出,就在磁盘上占用4个字节
在学习文件操作之前,我们要先对流和标准流有一定的理解
流:
我们程序的数据需要输出到各种的外部设备,当然也需要从外部设备中获取数据,不同的外部设备的输入和输出的操作各不相同,为了对各种设备进行方便的操作,我们抽象出了流的概念,可以抽象的理解成流淌着字符的河。
在C语言程序中对文件、画面、键盘等数据的输入输出操作都是通过流操作的。
一般情况下,我们想要向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
标准流:
我们知道了流的存在,那在我们写代码从键盘上输入数据,向屏幕上输出数据,为什么没有打开流呢?
#include<stdio.h>
int main(){
int n;
scanf("%d",&n);
printf("%d",n);
return 0;
}
我们可以看到在从键盘上输入与输出到屏幕上都没有打开流啊!
这是因为C语言程序在启动的时候,就默认打开了3个流:
这是默认打开的三个流,我们使用scanf、printf 函数就可以直接进行输入输出操作的。
stdin 、 stdout 、stderr 三个流的类型是FILE* , 通常称为文件指针 。
C语言中,就是通过FILE* 的文件指针来维护流的各种操作的。
文件指针:
在缓存文件系统中,关键概念就是 "文件指针类型" ,简称 ”文件指针“。
每一个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(比如:文件的名字、文件状态以及文件当前的位置等)。这写信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名为 FILE。
例如:VS 2013的编译环境提供的头文件 stdio.h 中有以下的文件类型声明:
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* pf; //文件指针变量
定义pf是一个指向FILE* 类型数据的指针变量。可以让pf指向某一个文件的文件信息区。通过该文件信息区中的信息就能够访问该文件。也就是(通过文件指针变量能够间接找到与它相关联的文件)
文件在读取之前呢,我们要先打开文件,在使用结束之后应该关闭文件。
在编写程序的过程中,在打开文件的同时,都会返回一个FILE* 的指针变量指向该文件,这也相当于建立了指针和文件之间的关系。
文件的打开模式和文件的关闭:
ANSI C规定使用 fopen 函数来打开文件,fclose来关闭文件。
//打开⽂件
FILE * fopen ( const char * filename, const char * mode );
//关闭⽂件
int fclose ( FILE * stream );
这里filename 指的是文件名,mode表示文件的打开模式,接下来看文件的打开模式:
接下来,我们来打开文件试一试:
"r"(只读):
此时,该路径下是没有text.txt 这个文件的,我们以只读的模式打开
#include<stdio.h>
#include<assert.h>
int main() {
FILE* pf = fopen("test.txt", "r");
assert(pf!=NULL);
fclose(pf);
pf = NULL;
return 0;
}
可以看到,当文件不存在时,以只读的形式打开,fopen就会返回一个空指针(这里我们需要注意,在打开文件以后,就需要判断是否打开成功,就要先判断pf是否为空指针,这里使用assert断言)
我们这里先创建一个test.txt 文件在该路径下,再以只读的方式打开:
我们就能看到,代码正常运行,而且文件也成功打开。
"w"(只写):
我们再以只写的方式打开一个文件,首先,现在
在这个路径下,是存在test.txt 这个文件的,我们先把它删除在以"w"的方式打开:
int main() {
FILE* pf = fopen("test.txt", "w");
assert(pf != NULL);
fclose(pf);
pf = NULL;
return 0;
}
代码没有报错,可以正常运行
这是,就新建了一个test.txt 的文件。
关闭文件:
flose 关闭文件,在使用close关闭完文件以后,一定要及时将文件指针变量置为NULL,(这个和free动态内存释放有点相似)。
知道了文件的打开和关闭,接下来,看文件的读写
上述说适用域所有输入流一般指适用于标准输入流和其他输入流(比如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(比如文件输出流)
以上是顺序读写的函数,接下来一个一个使用:
fgetc
现在我们想要从(流)文件中读取数据到程序中,现在我们test.txt 文本中有以下数据:
int main() {
FILE* pf = fopen("test.txt", "r"); //打开文件
assert(pf!=NULL);
//文件操作
int ch = fgetc(pf);
printf("%c\n", ch);//I
ch = fgetc(pf);
printf("%c\n", ch);//L
ch = fgetc(pf);
printf("%c\n", ch);//o
ch = fgetc(pf);
printf("%c\n", ch);//v
ch = fgetc(pf);
printf("%c\n", ch);//e
ch = fgetc(pf);
printf("%c\n", ch); //Y
ch = fgetc(pf);
printf("%c\n", ch);//o
ch = fgetc(pf);
printf("%c\n", ch);//u
ch = fgetc(pf);
printf("%c\n", ch);//!
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们可以看到整这个是按照顺序来一次读取一个字符的。
fputc
fputc函数是将字符输出到流(文件)中
这里我们向文件test.txt中写数据,把26个英文字母a -- z写到文件中
#include<stdio.h>
#include<assert.h>
int main() {
FILE* pf = fopen("test.txt", "w");//打开文件
assert(pf != NULL);
//写文件
for (int i = 'a'; i <= 'z'; i++) {
fputc(i, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
这里代码正常运行
我们来看是否将数据写到 test.txt 这个文本文件当中了
这里fputc将数据写到文件(流)中了
需要注意的是:fputc的第一个的参数是int型。
fgets
fgets函数的参数有三个,一个是str ,它是读取完数据要存放到的内存块的首地址; num是要读取的字符个数;stream就是文件指针
这里我们从test.txt文件中读取10个数据到程序中,并将其输出到屏幕上:
int main() {
FILE* pf = fopen("test.txt", "r");//打开文件
assert(pf != NULL);
//读文件
char str[50] = { 0 };
fgets(str, 10, pf);
printf(str);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
我们看到,读取10个字符,输出的却只有9个字符,只是因为,fgets在读取的过程中读取num-1个字符,在第num个位置会自动添加 '\0' ;并且,以 '\n'为结束标记,在读取到'\n'后,在末尾处会添加 '\0'构成字符串。
如果读取了num-1 个字符后,还没有读取到'\n'时,也会自动添加'\0',结束读取。
fputs
fputs 函数将str指向的数据,写到文件中去;
这里我们写"Hello,World!"到文件当中去:
int main() {
FILE* pf = fopen("test.txt", "w");//打开文件
assert(pf != NULL);
//写文件
char* str = "Hello,Wolld!";
fputs(str, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
代码正常运行
将数据写到了文件当中。
fscanf
之前我们使用scanf是从键盘(标准流)中按格式输入数据,现在fscanf,它可以从任意流中输入数据
相对于scanf,fscanf函数多一个FILE*类型的参数,这个就是流中输入数据
现在我们在文件中写 数据 zhangsan,18,man
按照格式输入(读取)
struct Student {
char name[20];
int age;
char sex[10];
};
int main() {
FILE* pf = fopen("test.txt", "r");//打开文件
assert(pf != NULL);
//读文件
struct Student s1 = { 0 };
fscanf(pf, "%s %d %s", &s1.name, &s1.age, &s1.sex);
printf("%s\n%d\n%s\n", s1.name, s1.age, s1.sex);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fprintf
我们之前使用的printf是将数据格式化输出到标准输出流中,而fprintf可以输出到任意的输出流中。
现在将数据输出到文件(流)中。
int main() {
FILE* pf = fopen("test.txt", "w"); //打开文件
if (pf == NULL) {
perror("fopen");
return -1;
}
//写文件
struct Student s1 = { "cuihua",18,"woman"};
fprintf(pf, "%s,%d,%s", s1.name, s1.age, s1.sex);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
scanf/fscanf/scanf:
scanf:scanf是从标准输入流中格式化读取数据,可以理解为
scanf(输入格式,具体输入内容);
fscanf:可以从任意流中格式化读取数据,(fscsanf也可以从标准流中读取时数据)可以理解为
fscanf(输入流,输入格式,具体输入内容);
sscanf:从字符串中读取数据,可以理解为
sscanf(字符型数组首地址,输入格式,具体输入内容);
接下来写代码来看一下sscanf:
int main() {
char arr[] = { '1','2','3','\0' };
char c1, c2, c3;
sscanf(arr, "%c %c %c", &c1, &c2, &c3);
return 0;
}
这里可以看到sscanf能够从字符串中读取数据,并存放到c1,c2,c3中。
fscanf从标准流中读取数据
int main() {
int a = 0;
fscanf(stdin, "%d", &a);
return 0;
}
stdin -- 标准流,用fscanf 也可以从标准流输入,只需要在fscanf第一个参数写stdin即可。
printf/fprintf/sprintf:
printf:输出数据到标准流当中,也可以理解为
printf(输出格式,具体输出内容);
fprintf:输出数据到任意流中,也可以理解为
fprintf(输出流,输出个格式,输出具体内容);
sscanf:输出数据到字符串当中,也可以理解为
sprintf(字符串数组首地址,输出格式,输出具体内容);
写代码来用一下sscanf:
int main() {
char arr[20] = { 0 };
char* str = "I Love You!";
sprintf(arr, "%s", str);
return 0;
}
可以看到,sprintf就是将数据输出到字符串数组中。
fprintf输出数据到标准流:
int main() {
int a = 0;
fscanf(stdin, "%d", &a);
fprintf(stdout, "%d", a);
return 0;
}
stdout -- 标准输出流,使用fprintf也可以输出到标准输出流,只需在将标准输出流传给fprintf函数即可。
以上是顺序读写,就是从文件开头按顺序来读写,那我们如果想要从文件某个位置开始读写,就要用到随机读写相关函数。
fseek函数
根据文件指针的位置和偏移量来定位文件指针(文件内容的光标)
stream就是流,offset是指偏移量,origin只有三个取值
SEEK_SET 文件起始位置
SEEK_CUR 文件当前位置
SEEK_END 文件结束位置
来实践一下,
int main() {
FILE* pf = fopen("text.txt", "w");
if (pf == NULL) {
perror("fopen");
return 0;
}
fputs("This is a apple ", pf);
fseek(pf, 10, SEEK_SET);
fputs("banana", pf);
fclose(pf);
pf = NULL;
return 0;
}
代码正常运行
我们再来看一下没有fseek这句代码时的结果:
int main() {
FILE* pf = fopen("text.txt", "w");
if (pf == NULL) {
perror("fopen");
return 0;
}
fputs("This is a apple ", pf);
fseek(pf, 10, SEEK_SET);
fputs("banana", pf);
fclose(pf);
pf = NULL;
return 0;
}
代码依然正常运行
可见,再输出完第一句后,文件当中的访问光标是在apple的后面,那我们也可以从当前位置开始调,向前调,offset就是负的。
ftell:
当我们在访问文件的时候,不知道文件访问光标访问到哪里了时,就可以使用ftell,这个函数返回文件指针相对于其实位置的偏移量。
int main() {
FILE* pf = fopen("text.txt", "w");
if (pf == NULL) {
perror("fopen");
return 0;
}
fputs("This is a apple ", pf);
//fseek(pf, 10, SEEK_SET);
fputs("banana", pf);
int ret = ftell(pf);
fclose(pf);
pf = NULL;
return 0;
}
可以看到返回的值时22,通过计算偏移量就是22。
rewind
让文件指针的位置回到文件的起始位置
这里我们先将数据输出到文件中,写完后,文件访问光标就走到了文件的最后,我们让文件访问光标回到文件的起始位置,在进行读取,最后输出到屏幕上。
#include <stdio.h>
int main()
{
int n;
FILE* pFile;
char buffer[27];
pFile = fopen("myfile.txt", "w+");
for (n = 'A'; n <= 'Z'; n++)
fputc(n, pFile);
rewind(pFile);
fread(buffer, 1, 26, pFile);
fclose(pFile);
buffer[26] = '\0';
printf(buffer);
return 0;
}
在我们程序运行的过程中,我们该如何去判断访问一个文件是否访问结束,在C语言中有这样一个函数:feof
feof函数
这个函数的作用是:当文件访问结束时,判断访问结束的原因是不是: 遇到文件末尾。
注意:这个函数不能用来判断文件访问是否结束。
函数使用
对于文本文件:判断函数返回值是否为EOF(fgetc),或者NULL(fgets)
fgetc访问判断是否为EOF;
fgets访问判断返回值是否为NULL;
对于二进制文件:
例如:
fread访问判断返回值是否小于实际要读的个数。
文本文件:
int main(void)
{
int c; // 注意:int,⾮char,要求处理EOF
FILE* fp = fopen("text.txt", "r");
if (!fp) {
perror("File opening failed");
return EXIT_FAILURE;
}
//fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取⽂件循环
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
}
二进制文件:
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = { 1.,2.,3.,4.,5. };
FILE* fp = fopen("test.bin", "wb"); // 必须⽤二进制模式
fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组
fclose(fp);
double b[SIZE];
fp = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 读 double 的数组
if (ret_code == SIZE) {
puts("Array read successfully, contents: ");
for (int n = 0; n < SIZE; ++n)
printf("%f ", b[n]);
putchar('\n');
}
else { // error handling
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp)) {
perror("Error reading test.bin");
}
}
fclose(fp);
}
ANSIC标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块 “ 文件缓冲区 ” 。从内存向磁盘输出数据会先送到内存中的文件缓冲区,装满缓冲区后才一起送到磁盘中。
如果从磁盘中向将数据读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
感谢观看,希望一下内容对你有所帮助,如果内容对你有作用,可以一键三连加关注,作者也正在学习中,有错误的地方还请指出,感谢!!!