常用的IO接口
库函数文件IO接口
1.在对文件的操作中,库函数提供了很多操作,但是我们常用的分别为以下五个:
FILE* fopen(const char* filename,const char* mode);
//打开文件(文件名为filename)。(以mode的方式)size_t fread(void* ptr,size_t size,size_t count,FILE* straem)
//读取文件
①:ptr:指向一个大小至少为sizecount字节大小的内存空间的一个指针。(这个指针也正是要将文件中需要读写的大小放入其中的)
②:size:要读取的每个元素的大小。
③:count:要读取的元素的个数。
其中,sizecount那就是全部读取数据的字节大小
④:stream:通过fopen打开的文件指针。size_t fwrite(void* ptr,size_t size,size_t count,FILE* straem)
//写入文件
参数与fread的参数相同,但是ptr是不同的。
fread的ptr是将读取的数据放入到ptr中,而fwrite是将ptr中的数据放入到文件中。int fseek(FILE* stram,long int offset,int origin)
//偏移文件的位置。(因为打开文件时,不同的打开方式会让操作文件内容的起始位置不同,所以我们可以通过此函数去设计我们想要操作的起始位置)
①:offset表示偏移量的大小。
②:origin表示的是从那个位置开始偏移,其中共有三处位置:
①:SEEK_SET----起始位置。
②:SEEK_CUR—当前位置。(也就是以打开方式打开文件的位置)
③:SEEK_END—文件末尾位置。int fclose(FILE* stream)
//关闭文件。
其中:
对于fseek成功返回0,失败返回-1;
对于fwrite函数,它的返回值为操作的字节个数。
而对于fread函数,它的返回值会出现歧义:
成功返回所有的字节个数,失败则返回0,读取到文件末尾了也返回0。
由于对0的返回我们无法判断到底是失败了还是读取到文件末尾了。
所以我们对fread进行操作的时候,对每次读取的元素设置为1个字节,而读取多少元素设置为总共要读取的大小的个数,这样我们就可以每次得到文件读取多少个字节,而不至于如果返回0,我们如何判断了。
还有一些判断返回值的函数,例如:
int feof(FILE* stream);//读写位置在末尾则返回真。
int ferror(FILE* stream);//指针出错则返回真。
2.关于打开文件的方式:
分别为以下:
mode | 功能 |
---|---|
r | 只读 |
r+ | 读写(前提是文件必须存在) |
w | 只写 |
w+ | 读写(文件不存在则自动创建,存在则清空) |
a | 追加 |
a+ | 追加+读 (文件不存在则自动创建,存在则追加到末尾) |
b | 以二进制方式打开 |
而我们最好打开的方式时都将b加上,因为对于库函数来说,它分文本操作还是二进制数据操作,但是对于系统系统调用接口,对文件的操作都是以二进制的方式进行的,因为它不分文本与二进制数据。而库函数是封装了系统调用的接口,所以我们最好还是在打开文件的方式上加上以二进制的方式打开。
3.三个输入输出流:
- stdin:标准输入。(一般是从键盘获取输入)
- stdout:标准输出。(一般是给显示屏显示输出)
- stderr:标准输出错误。(一般是给显示屏输出错误)
系统调用文件IO接口
1.在对文件操作时,系统也给了和库函数一样操作的一些函数,其中最常用的如下(其实,库函数就是封装了系统调用函数,所以他们的操作是相同的,但是参数以及函数名不同):
int open(const char* pathname,int flags,mode_t mode)
//打开文件
①:pathname:要打开或者创建的文件名。
②:flags:这是打开文件使用的一些参数,就如同库函数的mode方式一样,其中分类分别为以下:
O_RDONLY:只读打开。
O_WRONLY:只写打开。
O_RDWR:读和写打开。
其中以上这三个,必须得有一个。
O_CREAT:文件不存在,则创建。(这样就要写第三个参数mode了,忘了说,如果没有创建新文件,第三个参数就不需要写,如果创建了就必须写,第三个参数是设置文件的访问权限)
O_ADDEND:追加写。
O_TRUCE:存在则清空。
O_EXCL:文件存在则报错。
返回值:文件操作成功返回新文件的文件操作符,失败则返回-1。size_t read(int fd,void *buf,int len)
//读取文件
①:fd:打开文件返回的文件操作符。
②:buf:读取文件到buf这个空间中。
③:len:读取文件字节的长度。
返回值:成功返回读取文件的长度,失败返回-1。size_t write(int fd,void* data,int len)
//写入文件
①:fd:打开文件时返回的文件操作符。
②:data:即将写入文件的数据空间的首地址。
③:len:写到文件数据的大小。
返回值:成功返回实际写入文件的数据长度,失败返回-1。off_t lseek(int fd,off_t offset,int whence)
//偏移文件位置
①:fd:打开文件时返回的文件操作符。
②:offset:偏移量。
③:whence:偏移起始位置。(与库函数相同,也分别为:SEEK_SET / SEEK_CUR / SEEK_END)
返回值:成功返回你所指定相对于起始位置的位置,失败则返回-1。int close(int fd)
//关闭文件并释放资源。
文件操作符和文件流指针
1.文件操作符:open返回的一个非负整数。
说白了文件操作符其实就是一个地址的下标。
其实情况是这样的:
①:当我们用open打开文件的时候,这个时候在pcb所指向的struct files_struct结构体中会有一个sruct file* fd_arry[MAX]这个文件指针数组,其中当我们打开文件成功后,这个文件指针数组就会有一个位置指向到一个struct file结构体(其中,对于每个open的文件,他都会有一个属于字节的struct file,这里面表示的是文件描述信息),而struct file指向的便是这个文件,进而我们只返回其在文件指针数组的下标即可,而这个下标也就是文件操作符。
②:操作图如下:
所以当我们操作文件的时候,在pcb中找到文件结构体,在文件结构体中找到文件指针数组,然后根据文件描述符获取文件描述信息的地址,并通过描述信息操作文件。
注意:上图画的时候我将前三个文件指针数组中的位置空了起来,其实是有原因的,对于标准输出输入就是占的这三个位置。
当一个程序运行起来的时候,标准输入输出是默认被打开的三个文件:
其中0表示的是stdin(标准输入);1表示的是stdout(标准输出);2表示的是stderr(标准错误)。(0,1,2分别表示的文件指针数组的下标,也是上图中空起来的三个位置)。
2.文件操作符的分配原则:
原则:找到当前文件指针数组中没有使用的最小的下标作为新的文件描述符。
①:如下面代码:
结果如下:
②:而当我们将文件描述符1关闭时(就等于此时的标准输入被我们关闭了)
看下面代码:
结果如下:
此时的fd就为0,也就证明了我们的说法。
3.重定向:
对于重定向,我们在第一节的基本操作指令中有所提及。
标准输入重定向:>或者>>。(将原先打印的数据不在打印,而是写到指定文件中去)
①:>:清空重定向。(将需要写入的文件先清空,再将需要写入的数据写入)
②:>>:追加重定向。(将需要写入的数据写入到文件末尾)
而重定向的原理:改变当前文件指针数组的指针方向,如下图:
就像上图这样,会直接将0下标的指针数组的指向直接改变到test.txt文件。(但是文件操作符没有改变,只是改变了操作的文件)
4.使用dup2系统调用函数:
函数格式:int dup2(int oldfd,int newfd)
//其中oldfd为原始的文件操作符,newfd为想要改变的文件操作符。
如下列操作:
输出结果为:
由上图可以看出来,并没有直接在屏幕上展示出来,而是写入到了test.txt文件中,所以此时的标准输入已经指向了test.txt文件中。
对上述代码画图就如下:
所以我们在运行这个程序的时候,标准输出屏幕上并没有任何显示的东西,而查看test.txt时,发现这写从标准输入捕获的数据就在其中。
5.文件流指针和文件描述符
文件流指针和文件操作符是两个截然不同的东西,不可以混淆使用,前者用于库函数的操作,后者用于系统调用接口的使用。
它俩的区别如下:
①:文件流指针的数据类型为:FILE* ,而文件描述符的数据类型为:int。
②:在程序地址空间中,我们有过对用户和内核的空间分配情况,而在用户使用的库函数和系统调用的接口中,是库函数封装了系统调用的接口。所以在此时这两个对文件操作的函数也是如此。
③:使用以下代码:
运行结果如下:
五秒后:
从上图中我们可以看出来,对于write函数的使用,他是直接被标准输出到屏幕上,而printf和fwrite函数的使用是等待了五秒后被打印到标准输出屏幕上的,所以对于系统调用接口是没有缓冲区的,而封装了系统调用接口的库函数是有缓冲区的。
所以也验证了上一篇中对于exit和_exit函数的使用,出现了exit会刷新缓冲区,而_exit不刷新缓冲区。(因为其没有缓冲区)
所以对于文件流指针:由于它对系统调用接口的封装,所以它包含了文件操作符,也包含了缓冲区。
动态库和静态库的生成和使用
1.动态库和静态库的介绍:
- 静态库(后缀名:.a):程序在编译链接阶段把库中的代码链接到程序中,所以链接完成后,程序的运行便不再依赖库了。
- 动态库(后缀名:.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码(多个程序可以同时链接动态库)。
- 一个与动态库链接的程序,它只是链接了动态库中他所用到的函数的首地址的一个表,通过这个表来使用动态库里面的程序,而不是动态库的所有机器码。
- 动态链接:在可执行文件运行之前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中。
- 动态库可以被多个可执行程序链接,所以相比于静态库的使用,动态库使得可执行程序所占的空间更小,节省了磁盘的空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
2.动态库和静态库的生成:
库的命名:以lib为头,自己设置身子,后缀以.a为静态库,以.so为动态库。
库的概念:将许多已经写好的代码,打包成一个文件。(所以库中的不可以含有main函数)
①:静态库的生成:
首先:
- 将需要链接的程序先进行到汇编阶段完成,生成其.o文件。(但是必须注意的是:要生产库的.o文件时,必须在-c的前面加上fPIC,fPIC的作用是产生位置无关的代码)
- 生成静态库:对其使用-av -cr指令
使用例子如下:
假如我们要链接child.c文件
gcc -fPIC -c child.c -o child.o//汇编结束,生成.o文件
ar -cr libchild.a child.o //生成其静态库libchild.a(其中lib为前缀,.a为静态库的后缀)
ar -cr libchild.a child.o **.o **.o//如果需要的文件要多,那就依次在后面写出来即可
其中:ar是gnu归档工具,rc表示(replace and create)
②:生成动态库
首先:和上述生成静态库相同,先将需要链接的文件生成其.o文件(其中,fPIC不可少)。
其次对.o文件进行如下操作
以child.o为例:
gcc --share child.o -o libchild.so//其中就是在.o文件前面加上了--share,后面的命名规则和静态库相同,如果.o文件较多,那么就依次写在--share后面,或者使用makefile中的简便方法,例如:$^。
其中:shared: 表示生成共享库格式。
2.动态库和静态库的使用:
①:生成可执行程序的时候去使用,其中用 -l 去指定要链接库的名称(前提是该库要在 /usr/lib64 文件夹中,这时便可以直接使用-l去链接库名了)(并且-l与后面的文件名之前没有空格)
②:通过设置环境变量,将当前使用的库文件所在目录加入到环境变量地址中:
操作如:export LIBRARY_PATH=${LIBRARY_PATH}:./
然后使用上述例子:gcc test.c -o test -lchild -lm
:就生成了可执行程序。
如图:
这是对动态库的使用,但是对于上述环境变量的操作,与之对应的还有
:export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:./
:设置环境变量的库文件加载路径。(与上面的操作搭配使用)
③:对于静态库的操作,由于静态库的使用不依赖于库,所以一般使用-L操作:
使用gcc -L选项指定库所在的位置,然后进行如下操作:
还是child的例子:gcc test.c -o test -L./ lchild
//即可生成可执行程序test
如下图:
注意:对于gcc来说,默认使用的是动态库。