我们来看一下这个代码
cs
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 0;
scanf("%d", &a);
printf("%d\n", a);
return 0;
}
当我们输入100,打印出来100后,数据就会被销毁,也就是申请的内存空间被回收
当我们再次输入的时候,他就会再次给a创建空间
但是我们在使用手机信息的时候好像有的时候并没有被回收,这里就引出我们的内容:文件
数据存储在文件上就可以做到不丢失,文件在硬盘上面的,这个硬盘也就是所谓的c盘和d盘
从文件的功能我们会分为数据文件和程序文件
程序文件程序文件包括源程序文件(后缀为.c), 目标文件(windows 环境后缀为.obj), 可执行程序(windows 环境后缀为.exe)
源文件(.c)→[编译器]→目标文件(.obj)→[链接器]→可执行程序(.exe)
数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件
我们一般在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件
文件分为文本文件和二进制文件
数据在内存中都是以二进制的形式存储,如果不加以转换输出到外存的文件中,就是二进制的文件
如果在内存上异ascill码的形式存储,则需要在存储前转换,那这个就是文本文件
(二进制是存储的本质(数据在内存 / 文件中实际的样子) 十六进制是表示的工具(人类为了简化二进制读写而使用的编码方式),它不改变数据的底层存储形式(依然是二进制) 所有文件本质上都是二进制存储的,十六进制只是二进制的 "可读性转换")
区别:
文本文件 (如 .txt):存储的是字符的 ASCII 码 / Unicode 码的二进制形式。文本编辑器会把这些二进制码转换为人类可读的字符显示
二进制文件 (如 .exe、.jpg):存储的是程序指令、图像像素等 "无统一字符映射规则" 的原始二进制数据,必须用专门程序(操作系统、图片查看器)才能解析
简单点就是文本文件在我们打开像记事本这种文本编译器后会把这些的二进制转换为我们看得懂
二进制文件我们打开是根本看不懂的
那一个数据在文件是怎么存储的呢?
字符都是在ASCII形式存储,数值型数据既可以用ASCII也可以用二进制形式存储
假如有10000, 他在内存二进制是 00000000 00000000 00100111 00010000
如果用ASCII码存储,就是把这5个数字当成字符存储,每一个字符占据一个字节
1的ASCII值是49 >>>对应就是 00110001
0的ASCII值是49 >>>对应就是 00110000
1 0 0 0 0
所以以ASCII形式存储到硬盘就是 00110001 00110000 00110000 00110000 00110000
但是如果我们用的是二进制的形式存储到硬盘,就是四个字节
那就是上面的形式 00000000 00000000 00100111 00010000
但是我们用电脑打开显示的是 00 00 27 10 ,这个就是把上面的二进制转换成十六进制
四个四个为一组,0010是2,0111是7 ,组合一起就是00100111就是27,但是在电脑还有大小端的考虑这里就不说了
流和标准流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。C 程序对文件、画面、键盘等的数据流的输入操作都是通过流操作的。一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作
简而言之就是我们通过对于流的操作就可以操作外部的设备像屏幕,文件,网络这些,而流是怎么操作这些设备就不管我们的事了
但是我们学习c语言的时候好像没有打开流,那怎么对他进行操作,这是那是因为 C 语言程序在启动的时候,默认打开了3个流:
- stdin - 标准输入流,在大多数的环境中从键盘输入,scanf 函数就是从标准输入流中读取数据
- stdout - 标准输出流,大多数的环境中输出至显示器界面,printf 函数就是将信息输出到标准输出流中
- stderr - 标准错误流,大多数环境中输出到显示器界面。这是默认打开了这三个流,我们使用 scanf、printf 等函数就可以直接进行输入输出操作的
- stdin、stdout、stderr 三个流的类型是:FILE ,通常称为文件指针
C 语言中,就是通过 FILE 的文件指针来维护流的各种操作的
缓冲文件系统中,关键的概念是 "文件类型指针",简称 "文件指针"
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名 FILE
每当打开一个文件的时候,系统会根据文件的情况自动创建一个 FILE 结构的变量,并填充其中的信息,使用者不必关心细节
一般都是通过一个 FILE 的指针来维护这个 FILE 结构的变量,这样使用起来更加方便
定义 pf 是一个指向 FILE 类型数据的指针变量。可以使 pf 指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与它关联的文件
比如:
文件的打开和关闭
文件的打开和关闭文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。在编写程序的时候,在打开文件的同时,都会返回一个 FILE * 的指针变量指向该文件,也相当于建立了指针和文件的关系
ANSI C 规定使用 fopen 函数来打开文件,fclose 来关闭文件
fopen
FILE * fopen ( const char * filename, const char * mode );
文字内容:
功能:fopen 函数是用来打开参数 filename 指定的文件,同时将打开的文件和一个流进行关联,后续对流的操作是通过 fopen 函数返回的指针来维护。具体对流(关联的文件)的操作是通过参数 mode 来指定的。
参数:
- filename:表示被打开的文件的名字,这个名字可以绝对路径,也可以是相对路径
- mode:表示对打开的文件的操作方式,具体见下面的表格。
返回值:
- 若文件成功打开,该函数将返回一个指向 FILE 对象的指针,该指针可用于后续操作中标识对应的流。
- 若打开失败,则返回 NULL 指针,所以一定要对 fopen 的返回值做判断,来验证文件是否打开成功。

我们写一个代码看一下
cs
#include<stdio.h>
int mian()
{
FILE*pf=fopen ("data.txt","r")
if(pf==NULL)
{
perror("fopen");
return 1;
}
//然后读取文件
//然后关闭文件
}
我们用r就是读取一个存在的二进制文本,如果指定文件不存在就会出错,上面那个是相对路径,就是当前c语言的文件地址,如果在这个文件的上面我们可以用/..来表示上一级路径,/.就表示当前路径。所以我们可以写
cs
#include<stdio.h>
int mian()
{
FILE*pf=fopen ("./../test/data.txt","r")
if(pf==NULL)
{
perror("fopen");
return 1;
}
//然后读取文件
//然后关闭文件
}
那什么是绝对路径呢,就是从根源上开始,如果我们在文件放到桌面上,你的上一级不知道要上多少个级,可能还不行,这个时候我们就可以用绝对路径,在桌面上右键属性后找到地址
cs
#include<stdio.h>
int mian()
{
FILE*pf=fopen ("C\Users\zpeng\Desktop\data.txt","r")
if(pf==NULL)
{
perror("fopen");
return 1;
}
//然后读取文件
//然后关闭文件
}
fclose
函数原型:int fclose ( FILE *stream ); 还要把FLIE置为空指针
功能:关闭参数 stream 关联的文件,并取消其关联关系。与该流关联的所有内部缓冲区均会解除关联并刷新:任何未写入的输出缓冲区内容将被写入,任何未读取的输入缓冲区内容将被丢弃

参数:stream:指向要关闭的流的 FILE 对象的指针
返回值:成功关闭 stream 指向的流会返回 0,否则会返回 EOF


我们写一个代码看一下:这个就是在文件上输入

如果我们想要输入到标错流上也就是像屏幕一样就要用到stdout

如果我们想要打印输出一串字符也可以用到for循环


我们用printf,fputc和putchar都可以做到打印读取的数据,但是只能打印一个,要想全部打印可以用到循环

然后我们补充一下这个putchar函数:他和printf相比就是他只可以打印一个数据

feof 和 ferro
int feof ( FILE * stream ); //检测stream指针指向的流是否遇到文件末尾
int ferror ( FILE * stream ); //检测stream指针指向的流是否发生读/写错误
如果在读取文件的过程中,遇到了文件末尾,文件读取就会结束。这时读取函数会在对应的流上设置一个文件结束的指示符,这个文件结束指示符可以通过 feof 函数检测到。如果 feof 函数检测到文件结束指示符已经被设置,则返回非 0 的值,如果没有设置则返回 0
如果在读 / 写文件的过程中,发生了读 / 写错误,文件读取就会结束。这时读 / 写函数会在对应的流上设置一个错误指示符,这个错误指示符可以通过 ferror 函数检测到。如果 ferror 函数检测到错误指示符已经被设置,则返回非 0 的值,如果没有设置则返回 0
我们测试一下feof

我们再来测试一下ferror
我们在打开文件fopen后写入(w):如果文件存在会把文件内容清楚,如果不存在会创建一个文件
所以这个时候data就没有内容了,那我们用fgetc读取就没有用了,就会触发ferror


这个和上面的fputc不一样的是这个是这个是写入字符串的,那个是写入字符的


cs
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { "zhangsan", 20, 85.5f };
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fprintf(pf, "%s %d %f\n", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
其实和printf用法一样只不过是可以输出打印到任意文件不只限于屏幕(控制台)

cs
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = {0};
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
//fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
printf("%s %d %f\n", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}


cs
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "zhangsan", 20, 95.5 };
FILE* pf = fopen("data.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fwrite(&s, sizeof(struct S), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}

cs
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = {0};
FILE* pf = fopen("data.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
fread(&s, sizeof(struct S), 1, pf);
printf("%s %d %f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
scanf/printf - 针对标准输入 / 标准输出的格式化的输入函数和输出函数
fscanf/fprintf - 针对所有输入流 / 所有输出流的格式化的输入函数和输出函数(包括第一种的作用)
sprintf:将变量、结构体成员等数据,按照指定格式转换成字符串,写入到内存中的字符数组(缓冲区)
sscanf:从已有的字符串(字符数组)中,按照指定格式提取数据,赋值给变量或结构体成员,不直接从键盘 / 文件读取。


cs
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "zhangsan", 20, 95.5f };
char buf[100] = {0};
sprintf(buf, "%s %f %d", s.name, s.score, s.age);//"zhangsan 95.500000 20"
printf("%s\n", buf);
struct S t = {0};
sscanf(buf, "%s %f %d", t.name, &(t.score), &(t.age));
printf("%s %d %f\n", t.name, t.age, t.score);
return 0;
}
文件的随机读写

int origin 内容包括:文件开始的位置,文件指示器(指针的当前位置),文件末尾
这对应着这三个值
data.txt文件中存放着abcdef




SEEK_SET的意思是:文件开始的位置,3就是光标向后移动3位,abcdef中a光标向后3位就是d,所以打印出来就是a和d
我们还可以用SEEK_END:文件末尾的位置,然后向前移动3位就是-3,打印就是d了


就是相对与起始位置的偏移量
cs
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputs("abcdefghi", pf);
//写文件
fseek(pf, -3, SEEK_END);
fputc('x', pf);
fseek(pf, 0, SEEK_END); //光标到最后一位
int ret = ftell(pf); //计算从起始位置到光标的位置
printf("文件的字节数:%d ", ret);//就可以间接求出文件字节数
rewind(pf);//文件指针就回到文件的起始位置了
//等价于fseek(pf, 0, SEEK_SET);
fclose(pf);
pf = NULL;
return 0;
}

我们在存储数据的时候会放在缓存区,但是缓存区中有完全缓冲区:就是等到区域满了提取到外部
行缓冲区和无缓冲区


cs
int main()
{
FILE* pf = fopen("test.txt", "w+");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fputs("abcdefghi", pf);
fflush(pf);//刷新缓冲区
//读文件
fseek(pf, 0, SEEK_SET);
int ch = fgetc(pf);
printf("%c\n", ch);
//abcdefghi
fseek(pf, 2, SEEK_CUR);
fputs("xxx", pf);
//abcxxxghi
fclose(pf);
pf = NULL;
return 0;
}