目录
预备知识
复习C语言文件接口
fopen()
写入类:fwrite()、fprintf()、fputs()
读取类:fgets()
系统接口
open()
一个参数如何传递多个选项?
close()
write()
read()
预备知识
在正式讲解文件之前,我们需要有一些预备知识:
1.文件 = 文件内容 + 文件属性.
2.对文件操作:1.对内容操作 2.对属性操作
3.文件在磁盘(硬件)上,我们访问文件流程是什么呢?
先写访问文件的代码 ->编译->exe可执行程序->运行->访问到文件.
访问文件本质是进程在访问文件。而进程访问文件是需要接口的.我们目前所学到的接口都是一些语言接口,而不是系统接口.
磁盘是一种硬件,想要在硬件中写入,只有操作系统有权限。如果普通用户也想写入,那么操作系统必须提供文件系统调用的接口.
而我们平常使用的C语言的文件操作的相关接口都是对系统接口的封装,这样会使用户对接口更方便,简单的使用.
但是这样,会导致每个语言例如(C++和Iava)的文件操作的接口不相同,但是底层都是系统接口,因为这样的接口只有一套,只不过是封装方式不同。
封装的好处是可以跨平台运行代码(比如利用条件编译,动态裁剪),如果用户直接使用系统接口,换一个平台代码便运行不了了.
4.显示器是一种硬件,在linux下本质上是一个文件!printf向显示器打印,本质上也是一种写入;和在磁盘中向文件中写入是一样的!
5.Linux下,一切皆文件.我们现在对其只能有个理性的认识.
曾经我们理解的文件:read读取,write写入.
显示器:printf/cout ->一种写入
键盘:scanf/cin ->一种读取
以上都是站在我们写的程序的角度来说,将来我们的程序要加载到内存,相当于我们站在内存的角度,键盘相当于把我们的数据输交给内存(input),而内存把读取到的数据刷新到文件或者显示器当中(output)。
这就相当于是一次IO操作.
我们在回过头来,什么叫做文件呢?
站在系统的角度,能够被input读取,或者能够output写出的设备就叫做文件.
狭义上的文件:普通的磁盘文件
广义上的文件:显示器,网卡,声卡,显卡,磁盘等等,几乎所有的外设,都可称之为文件。
复习C语言文件接口
fopen()
我们先使用man fopen 来查看一下函数的用法
这个函数用来打开文件的,
1.FIFE* 我们称为文件指针。
2.其中第一个参数是要被打开函数的路径
3.第二个参数是打开模式,即以什么样的方式打开.一共有6种:
r(读):对文件只进行读操作
r+(读和写):对文件进行读和写操作.
w(读):先把文件内容清空,然后从文件开始进行写入操作.若文件不存在,则创建一个新文件.
w+(读和写):先把文件内容清空,然后可以问对文件进行读和写操作,若文件不存在,则创建一个新文件。
a(写):向文件结尾处开始写,相当于追加操作。若文件不存在,则会创建一个新文件.
a+(读和写):从文件结尾处开始写,文件开头处开始读,若文件不存在,则会创建一个新文件.
r是读取文件,这个在我先讲解完w(写)之后,然后讲解会方便一些.
我们看一下w:首先在vim中编写如下代码
#include<stdio.h>
int main()
{
FILE* fp = fopen("log.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
//进行文件操作
fclose(fp);
return 0;
}
这个我们使用了w模式打开,并且我们当前路径下并没有“log.txt”这个文件.
我们退出make编译,然后运行
这样因为原来文件不存在,所以会自动帮我们创建一个新文件.
那么问题是:我代码中只写了一个log.txt,那么是如何知道它在哪或者在哪创建呢?
当一个进程运行起来的时候,每个进程都会记录自己当前所处的工作路径!这个路径就叫做当前路径,log.txt的查找与创建都是在当前路径中进行的.
然后如果我们此时向log.txt文件中写入一些内容,然后再以w模式运行程序,会发生什么呢?
以w模式打开文件
然后向文件中输入“hello,world”并运行.
我们发现一开始log.txt中还有内容,然后以w方式运行程序后,里面的内容就被清空了.
这就验证了我们刚才所说的w模式会在写入之前将文件内容清空.
那如果我们如果不想让文件被清空,而是接着文件后面的内容写呢?这个时候需要用a/a+模式进行打开.
这个写的接口fwrite我们马上讲,知道这个是向文件中写入就好了
然后我们退出来,编译运行:
发现每一次运行并不会将文件清空,而是追加在后面继续写.
写入类:fwrite()、fprintf()、fputs()
我们刚才打开了文件,相应的就要对文件进行操作了,这是fwrite的用法:
1.第一个参数ptr是指向要被写入的元素数组的指针。
2.第二个参数size是每个元素的大小,字节为单位.
3.第三个参数是元素的个数,每个元素的大小为size字节
4.第四个参数为FILE指针,即fopen()打开文件之后返回的文件指针.
在代码中,我们这么写:
因为s1是一个字符串,所以strlen直接计算了整个字符串的大小,所以第三个参数只传入1即可,大小为整个字符串的大小.
此时我们输出log.txt便成功写入到文件了
然后介绍一下fprintf()的用法,函数原型为:
int fprintf(FILE *stream, const char *format, ...);
1.第一个参数还是为文件指针。
2.第二个参数就是我们正常的格式化输出。
用代码写入的时候,这样写
然后便也成功的写入到文件当中了:
fputs同样地也是向文件中写入,函数原型是
int fputs(const char *s, FILE *stream);
1.第一个参数为我们要写入的字符串内容.
2.第二个参数还是文件指针.即一开始的fp.
我们用一下,也比较简单
这样便成功写入到文件了.
读取类:fgets()
同样地,我们先来看一下函数原型:
char *fgets(char *s, int size, FILE *stream);
1.第一个参数s是输出型参数,会把读取的结果存放在s指向的字符数组里.
2.第二个参数size是读取的最大字节数,如果文件内容本身小于size,那么只读取文件内容,如果大于则读取size个字节
3.第三个同样是文件指针,通过fopen()得到的.
看一下它的返回值:
fgets()如果返回成功,则返回s,失败的话返回NULL.
具体是怎么使用呢?
然后我们此时log.txt文件中是已经有内容的,我们运行程序便可以看到内容被输出了
上面代码我们提到了stdout,这个是叫做标准输出流。
执行程序时,我们C语言会默认打开三个标准输入输出流:
1.stdin 标准输入,对应键盘
2.stdout 标准输出,显示器
3.stderr 标准错误, 显示器
我们用man stdin来看一下它.
发现类型是文件指针,其实这也变相的说明了一个道理:linux下一切皆文件!
但是如何理解这些,还是个问题,我们讲解完系统接口之后,相信再回来理解就会透彻很多.
系统接口
这里系统接口一共有4个:open,close,read,write
open()
它的作用是打开 并且可能创建一个文件或设备.暂时理解为打开文件即可.
1.第一个参数pathname,和刚才得接口一样,都是要打开的文件路径
2.第二个参数flags是一些选项,它的选项非常多,这里会挑一些常用的讲解.
3.第三个参数等下会说.
对于第二个参数:
flags中必须包含以上三个中的其中一个,分别代表O_RDONLY(read only只读),O_WRONLY(write only 只写),以及O_RDWR(read write读和写).
然后后面可以再添加别的选项.那么只有一个flags参数,怎么传入很多的选项呢?
一个参数如何传递多个选项?
这里采用了位操作:即用int中不重复的一个bit位,就可以标识一种状态.
例如0000 0001可以表示一种状态,0000 0010表示第二种状态,以此类推。
当我们需要检测有没有第一种状态时,我们可以让flag此时&0x1(0000 0001),这个时候,如果结果为1,说明flag的最后一位有1,那么就代表它有这个选项,便执行对应的操作.
同样的,检测是否有第二种状态,只需继续判断flag&0x2(0000 0010)==1。如果等于,说明flag的倒数第二位有1,否则没有.
那么如何让flag同时拥有这两种状态呢(即如何同时传入两个选项)?
我们只需要让两个状态 |(按位或) 一下即可.假设
#define ONE 0X1 // 0000 0001
#define TWO 0X2 // 0000 0010
然后 flag = ONE | TWO,此时flag的位便是 0000 0011
然后我们再判断flag,此时flag & 0x1 = 1,说明有第一个选项;flag & 0x2 = 1,也说明有第二个选项,这样我们就相当于传入了多个选项了.
我们使用简易的代码来完成一下这个操作.
此时我们期待的结果应该是输出1,3和1,2,3.
此时便得到了我们想要的结果了,也顺便成就了一个参数里面传递多个选项.
同样地,回到open上,它的flags中很多选项虽然是是大写,但也是定义的宏,本质上也是数字,然后分别用不同的比特位标识.
然后我们再看一下open的返回值:
可以看到open如果成功便返回一个新的文件描述符,失败则返回-1.
这个文件描述符我们下一章会详细讲解.现在你只要知道文件描述符是个整型即可.
知道了以上这些,我们开始用用open这系统接口吧.首先以只写的方式打开.
int main()
{
int fd = open("log.txt",O_WRONLY);
if(fd < 0)
{
perror("open");
return 1;
}
//open successful
printf("open success,fd: %d\n",fd);
}
我们此时目录下并没有log.txt文件,c语言那个fopen接口,只写的话如果没有文件会给我们创建一个新的文件,那系统接口会吗?
我们编译运行一下:
我们发现说系统找不到这个文件,由此说明,系统只写这个接口是不会直接给我们创建新的文件的,而c语言之所以可以,是因为对系统接口进行了封装,简化了我们的使用.
其实你在应用层看到的一个很简单的动作,在系统接口或者OS层面,可能都需要做很多的工作!
所以如果文件不存在想创建一个呢?这个时候便需要再传入一个选项O_CREATE.
然后我们将它按照上面所讲的,传递两个选项,则将两个选项 | 一下.
int fd = open("log.txt",O_WRONLY | O_CREAT);
此时我们便成功打开了.
但是,我们看一下我们创建好的文件的权限:
怎么会是这样的呢?我们不想要这样的,如果我们想指定权限该怎么做呢?
这就回到了我们刚开始讲open的时候第三个参数mode,它便是创建新文件时,赋予文件的权限.
比如我们给文件的权限是0666 .
int fd = open("log.txt",O_CREAT | O_WRONLY,0666);
这样,这个权限总算看着正常一点,但是怎么会少一个呢?
我们理想的是-rw-rw-rw,这个怎么是-rw-rw-r--呢,少的一个w去哪了?
这是由于掩码umask的存在,我在之前文章的权限的理解里详细的说过。
默认掩码是002,而每个权限又不能和掩码中的比特位为1的位相同,002中2的比特位是010,不能和第二个值为1的位一样,所以只能将w权限去掉,因为w正是对应的第二个比特位.
我们可以在程序里,将这个进程的umask设为0,然后便解决了问题.
此时便得到了正确的结果:
close()
这个接口比较简单,就直接把文件描述符传进去,然后关闭了文件即可.
write()
先来看write的用法
函数原型:
ssize_t write(int fd, const void *buf, size_t count);
1.fd为open()返回的文件描述符.
2.buf为要写入的内容
3.count为向文件中写入多少字节.
我们直接用代码来演示:
此时我们退出,编译运行
我们发现此时已经成功写入了.
如果此时我们把写入的内容换一下:
然后再次运行:
我们发现原来的hello,write没有被清空,而abcd是直接从头开始写的,然后把原来的一部分内容给覆盖了.我们如果想清空内容,就需要用到一个新的选项:O_TRUNC
这个选项会讲原文件内容清空. 所以我们把open代码中加上这个选项:
int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);
然后再次退出make编译运行:
发现此时原文件的内容已经被清空,然后写入了abcd.
如果我们想追加内容,即不清空,直接在文件后面继续写,我们把O_TRUNC换成O_APPEND即可. 这个便不再继续演示了.
read()
还是先来查看用法:
函数用法和write的非常类似,只不过把文件的内容读到buf,相当于buf成了输出型参数,读取的字节数是count.
最后read的内容会写在buffer中,然后我们最后输出它即可.
我们在log.txt文件中编辑以下内容:
然后编译运行程序:
便成功读取出来了.
关于文件操作的一部分内容便到这结束了,下一章我们将真正迈入文件的范畴,开始讲解文件描述符等相关的一系列知识.