【Linux】文件操作(一)

目录

预备知识

复习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文件中编辑以下内容:

 然后编译运行程序:

 

 便成功读取出来了.

关于文件操作的一部分内容便到这结束了,下一章我们将真正迈入文件的范畴,开始讲解文件描述符等相关的一系列知识.

 

Je suppose que tu aimes

Origine blog.csdn.net/weixin_47257473/article/details/131857917
conseillé
Classement