前言
为什么要使用文件呢?
你是否发现,我们写的程序,退出后再次运行,是记不住上次程序的数据的。这是因为我们写的程序是存储在电脑的内存中的,如果程序退出,内存回收,所以数据就丢失了。
如果想要将数据进行持久化的保存,我们可以使用文件。
想想我们电脑上的这些文件,PPT之类的,我们写好后再次打开依然是在的。包括我们写好的代码,也在电脑的文件夹里放着。这是因为这些文件是放在电脑的硬盘上的。
硬盘上的数据除非自己修改,否则是不会丢的。下次打开程序还是在硬盘上拿到我们的数据。
所以当我们把数据放在文件中,文件又放在硬盘上,就可以将数据进行持久化的保存。
当然这个“持久”,也不是永久,电脑或者硬盘是可能坏的。
什么是文件?
磁盘(硬盘)上的文件是文件。
我们电脑的C、D盘里进去的这些都是文件。
但在程序设计中,我们一般谈论的文件有两种:程序文件和数据文件(从文件功能的角度来分的)。
程序文件
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
比如在下图中,我们的test.exe可以从data.text中读取数据(读),或输出数据到data.txt(写)中,我们的test.exe是程序文件,data.txt是数据文件。
本文主要探讨的是数据文件,也就是怎么通过C语言写代码来操作数据文件。
以前,我们处理数据的输入输出都是以终端(也就是我们运行起来看见的黑框)为对象的,即从终端的键盘输入数据,运行结果显示在显示器上。
我们用printf打印数据到终端或者说屏幕,其实就是写的动作。
而从终端或屏幕输入或者读取数据,就是读的动作。
有时我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
经过本文的学习,以后我们写代码就可以将数据写到数据文件里。
文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含三个部分:文件路径+文件名主干+文件后缀
如:c:\code\test.txt中,c:\code\是路径,test是文件名主干,.txt是文件后缀。
方便起见,文件标识常被称为文件名。
前面是从功能的角度来讲,分为程序文件和数据文件,其实从内容的角度来讲,文件又可以分为二进制文件和文本文件。
二进制文件和文本文件
根据数据的组织形式,数据文件被称为文本文件或二进制文件。
数据在内存中是以二进制的形式存储的,如果不转换,输出到外存的文件中,就是二进制文件。
我们写上一个代码,它在内存中是以二进制存储的。
比如:
int main()
{
int a = 2077;
//2077的补码的二进制序列
return 0;
}
我们2077的存储方式是在内存中存它的补码的二进制序列。
浮点数在内存中存储也是以二进制。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在文件中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以用二进制形式存储。
字符没有办法,就只能以ASCII码形式存储。
现在有一个整数10000,如果以ASCII形式输出到磁盘,则磁盘中占用5个字节(每个字符占用一个字节),而二进制形式输出,则在磁盘上只占4个字节(整数的存储占用4个字节)。
注意:并不总是以二进制形式存储占用空间更少的,比如存1的时候,ASCII形式只占用1个字节,而二进制形式存储还是占用4字节。总之,要看具体情况。
具体点分析,10000的二进制序列:
0000 0000 0000 0000 0010 0111 0001 0000
按字节来排着看就是:
00000000 00000000 00100111 00010000
而将10000转换为ASCII形式存储,就是将其转换为1 0 0 0 0的分别存储,各自存储到一个字节(8bit)中去:
00110001 00110000 00110000 00110000 00110000
1 0 0 0 0
(下面是要存的字符,上面是这个字符的ASCII码值的二进制序列。1的ASCII码值是49,49的二进制序列是00110001;0的ASCII码值是48,二进制序列是00110000)
演示:
现在我们创建一个文件,重命名为test.txt
以这样的形式存放的时候,存的就是5个字符。
这就是文本文件 ,也就是说我们用肉眼能够看得懂的。
现在,我们还可以用代码的方式造出一个二进制文件:
#include<stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");//打开文件test.txt,
//w代表write,写。b代表binary,二进制。也就是二进制写的方式来打开文件
fwrite(&a, 4, 1, pf);//二进制的形式写到文件中,
//写a(提供的是地址),写4个字节,写1次,写到pf关联的文件中
fclose(pf);//关闭文件
pf = NULL;
return 0;
}
然后运行这个程序。
然后找到我们的test.txt,点击编辑:
所以可以看到,以文本文件的形式来解读我们写进去的二进制时,没法解读。
怎么证明它是个二进制呢?也可以证明一下:
右击源文件,添加现有项,将我们的test.txt加入。
然后再打开方式里选择二进制编辑器。
左侧的00000000不用看,右侧的才是文件内容。
刚才我们已经看到10000的二进制序列:
00000000 00000000 00100111 00010000
十六进制是:
0000 0000 0000 0000 0010 0111 0001 0000
0 0 0 0 2 7 1 0
00 00 27 10
因为是正整数,所以原码、反码、补码相同。但是是小端字节序存储,所以在内存中存的应该是:
10 27 00 00
所以就是将内存里的数据不加转换直接放到文件里去了。这种文件就是二进制文件。二进制通过文本编译器打开是读不懂的。(vscode打开也不行)
在介绍文件的打开和关闭之前,还有需要铺垫的基础知识:
流和标准流
流
程序的数据需要输出到各种外部设备,比如我们可以把数据放到文件、光盘、软盘(早期)、U盘、网络、屏幕上等。
也需要从外部设备获取(读取)数据。
不同的外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念。
写程序只要关注流的操作就可以了,程序先写到流里,流再对接各种各样的外部设备。
可以把流想象成流淌着字符的河。
至于流怎么把数据给这些外部设备,就是底层的事情了。
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。
一般情况下,我们想要向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
一般的三个步骤:
1.打开流
2.读/写
3.关闭流
标准流
针对不同的外部设备,流分为很多种,有文件流、标准流等。
可以回想一下我们平时写代码:
scanf,从键盘上读取数据;printf,将数据写到屏幕上。我们的键盘和屏幕也是外部设备,但是好像并没有打开流这样的具体操作。为什么呢?
这是因为C语言程序在启动(运行)的时候,默认打开了3个流:
stdin——标准输入流,在大多数的环境中是从键盘输入,scanf函数就是从标准输入流(键盘)中读取数据。
stdout——标准输出流,在大多数环境就是我们的显示器界面(屏幕),printf函数就是将信息输出到标准输出流中。
stderr——标准错误流,大多数环境中输出到显示器界面。
这三个流是默认打开的,所以我们在使用scanf、printf等函数时直接就对标准流进行了操作,并没有去关心这个流怎么打开。
stdin stdout stderr三个流的类型是:FILE*,通常称为文件指针。
C语言中,就是通过FILE*的文件指针来维护流的各种操作的。
通过文件指针,我们就能找到流,然后再通过流的操作,操作外部设备。
到此,文件操作(上卷)就结束了,之后还会有文件操作(下卷),祝阅读愉快^_^