在我们运行程序的时候,会在内存中申请空间来存储这些数据;如果我们退出程序的话,系统就会回收内存,那么数据就全部丢失了,如果我们将这些数据储存在文件中,就可以长久的保存起来;
在程序设计中一般常见的有2种文件,一种上程序文件,一种是数据文件;
程序文件:
像c程序生成的后缀为. c的源文件,后缀为obj的目标文件,还有exe的可执行文件;
数据文件:
文件也可以用来存储运行程序时的数据内容,这些数据文件可以让我们在程序运行时从中读取数据到,或者写入数据在里面;
这里主要提到的也就是数据文件;
我们在c语言中输入和输出都是在控制台上进行的,有时候我们可以会从磁盘读取文件,或者往磁盘写入数据;
文件名的组成:
包含三部分:文件路径+文件名主干+文件后缀;
比如:
"C:\Users\86132\Desktop\data.txt"
数据文件通常分为2种,一种是二进制文件,一种是文本文件;
这两种有何区别呢?
二进制文件:
数据在内存中都是以二进制的形式进行存储的,我们将这个数据不加转换直接存入外存,这就叫做二进制文件;
文本文件:
将数据转换为ASCLL码值存储在外存中,这就叫文本文件;
通俗来讲,文本文件我们肉眼是可以读懂的,而二进制文本我们是无法读懂的;
字符只能通过ASCLL值进行存储,而数值既可以用ASCLL码的形式存储,又可以用二进制的形式存储;
我们来看看数据在内存中又是怎样存储的呢
我们将10000存储在数据中以ASCll码值的形式存储,实际上是以ascll值的二进制形式存储;将数据以ascll码的形式存储是把每一位看作一个字符进行存储;也就需要5个字节才能存下;
10000是一个整形,在内存中以2进制的形式存储只需要4个字节,而这是小端机器,所以是倒着存放的;
我们通过程序来实现一下:
int main() { int n = 10000; FILE* pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "wb"); if (pf == NULL) { perror("fopen->C:\\Users\\86132\\Desktop\\data.txt"); return 1; } fwrite(&n, sizeof(char), 5, pf); fclose(pf); pf = NULL; return 0; }
上面就是我们将10000以二进制的形式写入文本中,这也就是二进制文件,我们可以看到我们根本无法读懂,但这并不代表机器读不懂;
我们将文本中的乱码解析一下是不是就是10000,是不是和我们上面画的图一模一样;
关于上面函数的内容和含义后面都会有详细的讲解;
流和标准流:
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出
操作各不相同,为了⽅便程序员对各种设备进⾏⽅便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。
C程序针对⽂件、画⾯、键盘等的数据输⼊输出操作都是通过流操作的。
⼀般情况下,我们要想向流⾥写数据,或者从流中读取数据,都是要打开流,然后操作。那么我们为什么在键盘上输入数据和在屏幕上打印数据没有开启流呢?
其实c程序在启动的时候就默认帮我们打开了3个流;
1.标准输入流------stdin一般是指键盘输入,比如scanf就是标准输入流;
2.标准输出流------stdout一般是指输出到屏幕上,比如printf就是标准输出流;
3.标准错误流------stderr;
正因为打开了这三个流我们才能使用scanf/printf进行标准输入/输出;
这三个流的类型是FILE*,也就是文件指针;
文件指针是用来维护流的各种操作的;
文件指针又是什么呢?
其实在我们打开一个文件的时候,程序会向内存申请一块空间来存放文件的各种信息,也叫做文件信息区;这个区域是定义在一个结构体类型里面的,是由c系统进行命名的,叫FILE;
例如,VS2013编译环境提供的 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* pf;
比如pf就是一个指向某个文件的流;
我们知道了这些,那我们如何打开文件呢?
这里有一个函数fopen,这个函数有2个参数,第一个filename也就是要打开的文件的名字;
第二个参数就是mode,以何种方式去操作这个文件,也就是打开这个文件是要去往里面写数据还是往程序中读数据;这个函数就是用来打开文件的;
传文件的名字可以传文件的绝对路径或者只写文件名;如果只写文件名会默认在当前c程序所在的文件所在地里面找;如果这样写的话 . / .. / 文件名;意思是在当前文件的所在位置的上一级位置中读入/写入数据;
int main() { FILE* pf = fopen("./../data.txt", "w"); return 0; } //这里是测试就没有去判断pf;
我们也可以看到确实在上一级文件中创建了一个新的data.txt文件
(我这里是隐藏了文件的后缀,如果不写后缀,所创建的文件的打开的方式可能不同,但内容不会改变;切记:如果要读写数据,给的文件名的后缀和名字一定要和你创建的文件相同,不然系统是找不到这个文件的!)
比如:
int main() { FILE* pf = fopen("./../data", "w"); return 0; }
按照上一个程序的话我肯定可以找到那个文件的,但是因为我没有写后缀,系统是找不到我的文件的,并且新创建了一个别的文件;
这个函数的参数mode有以下取值,
r------读数据,也就是从文件中读取数据到输入缓冲区;如果没有这个文件的话就会打开失败报错;
w------写数据,就是向这个文件中写入数据到输出缓冲区;没有文件就会创建一个新的文件;
a------追加,在文件的末尾追加数据;没有文件就会创建一个新的文件;
r+------读取数据和写入数据;如果没有这个文件的话就会打开失败报错;
w+------读取数据和写入数据;没有文件就会创建一个新的文件;
a+-------在文件末尾出进行追加读写;没有文件就会创建一个新的文件;
b----以二进制的形式...
b一般都和上面配合使用,比如以二进制的形式写入/读入数据;
这个函数打开文件成功会默认返回指向这个文件的FILE指针(文件指针);
用完文件之后要把文件关闭了,而关闭文件的函数就是fclose:
这个函数的参数是指向文件的流;那我们将这个FILE的指针变量传给函数就行了;
当然我们传递的只是变量的数值,并没有将指针变量修改了,所以关闭文件后要及时把指针置为NULL;
1.fgetc/fputc
参数是要读取的文件的流(文件指针);
代码演示:
int main() { FILE* pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } char ch[8] = { 0 }; int i = 0; char* pc = ch; for (i = 0; i < 7; i++) { pc[i] = fgetc(pf); } printf("%s", ch); fclose(pf); pf=NULL; return 0; }
我们文本中的内容是abcdefg这个程序可以帮助我们从文本中将数据类似拷贝到这个字符串数组中;
fgect每次读一个字符;其实在文本中有一个光标,在我们进行读/写的时候光标是停留在最开始的地方,当我们读/写数据的时候,光标会依次往后走,知道读完整个文本(如果不加限制或者读取失败)直到文件结尾;其实这就是所谓的顺序读写;
pc是一个char*的指针,每次加一跳过一个字节;p[i]相当于*(p+i),p+i是地址,解引用就找那块空间;
我们以字符的形式写入字符数组是没有\0的,所以我们要把数组初始化为0(\0的ascll码值是0;)不然用%s打印会打印出乱码;
第一个参数是要写入的字符内容,第二个参数就是要写入的流;
代码演示:
int main() { FILE* pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } fputs("w",pf); fclose(pf); pf = NULL; return 0; }
我们确实将字符w写入了文本中;
2.fgets/fputs
它的第一个参数是要读到哪一个字符串,第二个是读几个,第三个是从哪个流里读(文件指针);
其中我们要注意第二个参数,它实际是读入num-1个字符个数;也就是我想要读入7个字符,但最终只会给我读入6个字符,还有一个位置是留给\0的,下面我们来讲解;
代码演示:
int main() { FILE* pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } char ch[] = {"xxxxxxxxxxxxxxx"}; fgets(ch, 7, pf); fclose(pf); pf = NULL; return 0; }
我们文本中写的是abcdefg,是7个字符,而我们也写了读7个字符,但是它最后只读到了f就停下了,专门留了个位置给\0;
第一个参数是从哪个字符串中读取数据,第二个是要写入的流(文件指针);
代码演示:
int main() { FILE* pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } char ch[] = {"abcdefg"}; fputs(ch, pf); fclose(pf); pf = NULL; return 0; }
3.fscanf/fprintf
第一个参数是从哪个流(文件指针)里读入数据;第二个参数就是格式化数据(可变参数列表);
我们可以对比一下scanf;
我们可以看到2者只有第一个参数有差异;scanf是格式化输入数据,是从键盘上读入;而fscanf是从文件中格式化读入数据;其实用法和scanf差不多,只是获取数据的方式不一样;
代码演示:
struct S { char w; int n; float f; }j; int main() { struct S j = { 0 }; FILE* pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } fscanf(pf, "%c %d %f", &(j.w), & (j.n), &(j.f)); printf(" %c %d %f", j.w, j.n, j.f); fclose(pf); pf = NULL; return 0; }
我们似乎发现实现的逻辑似乎和scanf一样;
scanf是从键盘上读入数据,当发现不满足格式时或保存下一个字符,下一次调用scanf时从上次遗留下来的字符开始读取,如此往复,直到读取结束或者读取失败;
fscanf也是这个逻辑;
第一个参数是写入的流(文件指针);第二个格式化写入数据(可变参数列表);
我们同样对比一下printf:
你还真别说,依旧只是获取数据的方式的差异;
代码演示:
struct S { char w; int n; float f; }j; int main() { struct S j = { 'w',32767,3.14f}; FILE* pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "w"); if (pf == NULL) { perror("fopen"); return 1; } fprintf(pf, " %c %d %f", j.w, j.n, j.f); fclose(pf); pf = NULL; return 0; }
注意:上面介绍的三组标准输入/输出流适用于各种输入/输出流(键盘输入/输出,文件输入/输出;)
下面介绍的只能在文件输入/输出流中使用:
4.fread/fwrite
fwrite是以2进制形式写入文本;fwrite只适合文件输出流;
第一个参数是从哪个内存块读出数据;
第二个参数是读多少数量;
第三个参数是读的数据的大小是多少,单位是字节;
第四个是读入的流(文件指针);
代码演示:
int main() { int* pa = (int*)malloc(4); *pa = 10000; FILE* pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "wb"); if (pf == NULL) { perror("fopen"); return 1; } fwrite(pa, sizeof(int), 1, pf); fclose(pf); pf = NULL; return 0; }
我们将10000以2进制的形式写入文本中,我们可以来看看:
因为是以2进制的形式写入的,所以我们没法直接读取它的内容,但是机器可以读取,接下来我们介绍与之对于的二进制输入流;
二进制输入流,这个函数依旧只适合文件的输入流(只读二进制形式的数据不读文本数据);
第一个参数是指向一块内存大小至少为size*count大小的内存块;
第二个参数是读入每个数据大小单位是字节;
第三个参数是要读入元素的数量单位是字节;
第4个参数是从哪个流(文件指针)里读数据;
代码演示:
int main() { int* pa = (int*)malloc(4); FILE* pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "rb"); if (pf == NULL) { perror("fopen"); return 1; } fread(pa, sizeof(int), 1, pf); printf("%d", *pa); fclose(pf); pf = NULL; return 0; }
确实读取的是10000;
2.文件的随机读写
既然能有序的读写数据,那么也可以无规则的读写数据;
其实我们之所以能从文本中读写数据,其实是因为我们打开一个文件的同时文本会生成一个光标,当我们读取一个数据之后,光标就会往下走读取下一个数据,以此类推直到读完所有数据(如果顺利读取的话);
我们来看代码:
int main() { char ch = 0; FILE* pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); fclose(pf); pf = NULL; return 0; }
我们从文本中读4次字符;是不是只读了abcd,这说明确实如我们所说;每读取一次光标会往下走;
接下来介绍一个函数:
这个函数有3个参数,第一个是指向这个文件的流(文件指针);第二个是文件指针的偏移量,第三个是从什么位置开始算偏移量;
第三个参数有三个选择:
SEEK_SET------文件指针的起始位置(通俗来讲就是光标最开始的位置)
SEEK_CUR___文件指针的当前位置(光标现在的位置)
SEEK_END------文件指针的最后(文件结束的位置)
那么这个函数到底是怎么用的呢?
我们一起来看一下;
int main() { char ch = 0; FILE* pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); fseek(pf, -3, SEEK_CUR); ch = fgetc(pf); printf("%c\n", ch); fclose(pf); pf = NULL; return 0; }
同理,SEEK_END就是从文件末尾算偏移量,SEEK_SET就是从文件开头算偏移量;
那么有时候我们可能根本不知道光标已经走到哪里去了,这个时候我们可以借助另外一个函数:
这个函数会返回光标当前距离文件起始位置的偏移量,我们只需要给他穿指向那个文件的流就行;
代码举例:
int main() { char ch = 0; FILE* pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); ch = fgetc(pf); printf("%c\n", ch); long m = ftell(pf); printf("%ld\n", m); ch = fgetc(pf); printf("%c\n", ch); fclose(pf); pf = NULL; return 0; }
或者我们也可以直接回到文件的起始位置:
我们也只用给这个函数传指向文件的流即可;
代码演示:
int main() { char ch = 0; FILE* pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "r"); if (pf == NULL) { perror("fopen"); return 1; } ch = fgetc(pf); printf("%c\n", ch); 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; }
3.三组输入输出函数的区别
scanf/printf
fscanf/fprintf
sscanf/sprintf
前面2组输入输出函数我们前面都讲解过,重点看第三组;
scanf/printf是针对标准格式化输入输出流的,也就是我们从键盘输入数据,屏幕上打印数据;
fscanf/fprintf是针对所有格式化输入输出流的输入输出,也就是说我们可以用它从不同的输入输出方式获取数据;我们要分清楚前面2组的区别;
第一个参数是从哪块空间检索数据,第二个参数就是跟scanf一样的效果啦(格式化输入);
int main() { char(*pa)[10] = (char(*) [10])malloc(10); char** pf = (char**) & pa; *pf = "Rudolph 12"; char ch[10]; int a; sscanf((char*)pa, "%s %d", ch, &a); printf("%s %d", ch, a); return 0; }
第一个参数就是字符串,第二个参数是格式化输出;
int main() { char* str = (char*)malloc(10); char ch[10] = {"hello bit"}; int j = 100; sprintf(str, "%s %d", ch, j); printf("%s", str); return 0; }
总结一下,sscanf是将字符串转换为格式化数据;sprintf是将格式化数据转换为字符串;
当⽂件读取结束的时候,判断是读取结束的原因是否是:遇到⽂件尾结束。
如果是遇到文件结尾结束的,则返回非0的值;反之返回0;
与之对应对另外一个函数:ferror;
这个函数是判断读取的原因是否是因为读取错误导致的读取结束;
不同的函数有不同的判断方法,比如:
用fgetc来读取数据的话我们可以判断它的返回值,如果这个函数读取结束或者读取失败都会返回EOF,这个时候我们就可以通过feof和ferror来判断停下来的原因是什么;
代码演示:
#include
#include
int main(void)
{
int c; // 注意:int,⾮char,要求处理EOF
FILE* fp = fopen("test.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);
}
如果是fgets的话,如果它读取失败或者读取错误都会返回空指针;这个时候我们就可以判断导致返回空指针的原因究竟是什么;
代码演示:
int main()
{
FILE * pf = fopen("C:\\Users\\86132\\Desktop\\data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char* ch = (char*)malloc(10);
char *pc = fgets(ch, 7, pf);
if (feof(pf))
{
puts("正常读取完成");
}
else if (ferror(pf))
{
puts("读取错误导致的结束");
}
fclose(pf);
pf = NULL;
return 0;
}
如果是二进制文件读取(fread)的话,我们只需要判断函数的返回值和我们要读取的数量相不相等即可;如果不相等的话是因为什么原因导致的(可能因为读完了,也可能因为读取错误了);
代码演示:
#include
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编译系统决定的。
这样提升了传输的效率,我们在对文件进行读入/写入操作的时候,并不是直接由fgets/fputs这些函数直接操作的,而是通过这些函数去调用操作系统来完成的读/写;如果没有文件缓冲区的话,我们输入一次字符就要调用一次操作系统进行读/写,频繁的调用就导致了工作效率的下降;
我们可以通过代码感受一下效果:
#include
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);
//注:fclose在关闭⽂件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}