1.前置预备
- 文件 = 内容 + 属性
- 访问文件之前,都必须先打开他
cpp
#include<stdio.h>
int main()
{
FILE* fp=fopen("log.txt","w");
if(fp==NULL)
{
perror("fopen");
return 1;
}
fclose(fp);
return 0;
}
把源代码编成可执行程序,并没有打开文件,当程序运行执行到fopen时,才被打开,fopen和malloc一样,都是运行时操作,
当访问一个文件时,是进程在进行访问,可是进程是在内存中被CPU调度,文件是在磁盘中的,
根据冯诺依曼,CPU不能直接访问到磁盘,所以,文件也必须被加载到内存中。
所以打开文件fopen,是在做什么?把文件加载到内存中
进程 = 属性 + 内容,同样 ,文件 = 内容 + 属性,OS需要对进程进行管理,需不需要对加载到内存的文件进行管理呢?必须要!!!
如果管理文件呢?先描述,再组织,
在内核中,文件 = 文件的内核数据结构 + 文件的内容,
磁盘中,文件 = 文件属性 + 内容
结论:我们研究打开的文件,本质是在研究进程和文件的关系。没有被打开的文件呢?在磁盘上
文件:
1.被打开的文件------加载到内存,
2.没有被打开的文件------磁盘
2.以"w"方式打开文件
先看用C语言打开一个文件,用"w"的方式打开一个文件log.txt,如果这个文件不存在,就新建,如果这个文件存在,则清空内容后打开,
在命令行上,可以通过**> 文件** 。对一个文件进行操作,> 意思跟"w"一样,文件不存在则创建,存在则对内容进行清空, > 也叫输出重定向。
下面一段代码内容是文件不存在,新建然后进行数据插入:
cpp
#include <stdio.h>
int main()
{
//'w'文件不存在新建
FILE *fp = fopen("./log.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
const char *message = "hello file\n";
int i = 0;
while(i<10)
{
fputs(message,fp);
i++;
}
fclose(fp);
return 0;
}
下面一段代码是打开文件,然后关闭文件,不进行任何操作,会对文件内容进行清空:
cpp
#include <stdio.h>
int main()
{
//'w'文件不存在新建
FILE *fp = fopen("./log.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
// const char *message = "hello bit\n";
// int i = 0;
// while(i<10)
// {
// fputs(message,fp);
// i++;
// }
fclose(fp);
return 0;
}
3.以"a"方式打开文件
在命令行上,通过 >> 文件。对文件进行操作,>> 和"a"意思一样,对内容进行追加
先用"w"方式打开文件,每次执行该程序,都会先进行清空,无法进行追加:
cpp
#include <stdio.h>
int main()
{
//'w'文件不存在新建
FILE *fp = fopen("./log.txt","w");
if(fp == NULL)
{
perror("fopen");
return 1;
}
char buffer[1024];
const char *message = "hello bit";
int i = 0;
while(i<10)
{
snprintf(buffer,sizeof(buffer),"%s:%d\n",message,i);
fputs(buffer,fp);
i++;
}
fclose(fp);
return 0;
}
这时可以用"a"方法进行打开文件,每次执行该程序,都会追加式的像文件进行插入:
cpp
#include <stdio.h>
int main()
{
//'a'追加内容
FILE *fp = fopen("./log.txt","a");//append
if(fp == NULL)
{
perror("fopen");
return 1;
}
char buffer[1024];
const char *message = "hello bit";
int i = 0;
while(i<10)
{
snprintf(buffer,sizeof(buffer),"%s:%d\n",message,i);
fputs(buffer,fp);
i++;
}
fclose(fp);
return 0;
}
4.程序默认打开三个输入输出流
一个程序默认启动,会打开三个输入输出流,标准输入,标准输出,标准错误,在C语言中,底层硬件所对应的文件键盘与显示器,把他们包装成文件的样子,最后访问键盘显示器,就可以以文件FILE*指针的形式进行访问
总结:是进程会默认打开三个输入输出流
看看下面代码,把内容打印到显示器方法:可以通过stdout进行操作
cpp
#include <stdio.h>
int main()
{
printf("hello word\n");
fputs("bit\n",stdout);
fwrite("aaaaa\n",1,4,stdout);
fprintf(stdout,"bbbb\n");
return 0;
}
2.文件管理
往显示器上显示,往磁盘文件打开或写入文件,读写键盘,本质上是访问硬件,我们用户在访问硬件,不可能直接通过语言进行直接访问硬件的,必须要通过操作系统,
我们使用的C接口,看起来是直接访问硬件,其实是通过操作系统提供的系统调用接口,才能访问到硬件,所以我们使用的C接口,底层一定要封装对应的文件类的系统调用!!
fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数 (libc)。
⽽ open close read write lseek 都属于系统提供的接⼝,称之为系统调⽤接⼝
1.open
现在我们来认识一下文件系统调用接口:我们操作文件,首先是要打开文件,系统调用open。
第一个参数是文件名,
第二个参数,实际上是一个32bit位,也就是一个位图
第三的参数,如果文件已经创建,就不需要带,如果文件没有被创建,就需要对文件进行权限赋值
我们通过下面一段代码来认识,位图参数的传递:
cpp
#include <stdio.h>
#define ONE (1<<0)//1 000001
#define TWO (1<<1)//2 000010
#define THREE (1<<2)//4 000100
#define FOUR (1<<3)//16 001000
#define FIVE (1<<4)//32 010000
//code 1
void PrintTest(int flags)
{
//都为1才为1,只要有一个不为1,就为0
if(flags & ONE)
{
printf("one\n");
}
if(flags & TWO)
{
printf("two\n");
}
if(flags & THREE)
{
printf("three\n");
}
if(flags & FOUR)
{
printf("four\n");
}
if(flags & FIVE)
{
printf("five\n");
}
}
int main()
{
printf("=====================\n");
PrintTest(ONE);
printf("=====================\n");
PrintTest(TWO);
printf("=====================\n");
PrintTest(THREE);
printf("=====================\n");
//只要两个操作数对应的位中有一个为1,那么结果位就为1。
PrintTest(ONE | THREE);
printf("=====================\n");
PrintTest(ONE | TWO | THREE);
printf("=====================\n");
PrintTest(ONE | TWO | THREE | FOUR);
printf("=====================\n");
return 0;
}
所以我们可以通过 | 的方式进行传递参数,通俗点意思就是说,| 两边条件都满足。
open函数第二个参数:
- O_RDONLY:以只读方式打开文件。
- O_WRONLY:以只写方式打开文件。
- O_RDWR:以读写方式打开文件。
- O_CREAT : 若⽂件不存在,则创建它。需要使⽤mode选项,来指明新⽂件的访问 权限
- O_TRUNC:如果文件已存在且成功打开,则将其长度截断为 0。
- O_APPEND:写入时将数据追加到文件末尾。
返回值: 成功:新打开的⽂件描述符 失败:-1
现在我们来使用一下open函数,第二个参数传递 O_WRONLY | O_CREAT,以只写方式打开,如果文件不存在则创建:
cpp
#include <stdio.h>
#include <fcntl.h>
int main()
{
//以只写方式打开,如果文件不存在则创建
open("log.txt",O_WRONLY | O_CREAT);
return 0;
}
这里我们发现新创建的文件,他的权限是错乱的,那是因为我们调用系统接口时,新创建文件,需要给文件进行权限设置,而我们语言级接口fopen不需要,是因为底层对其进行了封装。
所以用系统调用接口时,新建文件,还要告诉新建文件默认的起始权限是多少!!!!也就是第三个参数传递,传递权限。
下面代码我们给文件进行权限赋值666,意思就是文件权限为rw-rw-rw-,r = 4 w = 2 x = 1.
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
//以只写方式打开,如果文件不存在则创建
open("log.txt",O_WRONLY | O_CREAT,0666);
return 0;
}
此时我们就可以发现权限没有错乱,但同时又有一个问题第三方的全是为什么只要一个r?
因为系统里有个umask,他会默认屏蔽掉一些权限,系统的umask = 0002,结合666,就会编成664,所以文件的最终权限会结合umask值来进行最终确认。
我们在编写代码的时候,也可以进行设置umask值,如下代码所示:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
//不让系统屏蔽某些权限
umask(0);
//以只写方式打开,如果文件不存在则创建
open("log.txt",O_WRONLY | O_CREAT,0666);
return 0;
}
对umask清0,最终文件权限就是是第三个参数传递的权限,对在代码中umask清0,并不会影响到系统的umask值。
1.每一个进程有一个umask值,表示创建文件umask权限,进程umask权限默认从系统中获得,但自己手umask权限,采用就近原则来直接使用用户的umask值
2.touch创建文件其实权限都是666,然后受umask影响编程664.
下面代码模拟实现touch:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//模仿touch
int main(int argc,char *argv[])
{
//不让系统屏蔽某些权限
//umask(0);
//以只写方式打开,如果文件不存在则创建
open(argv[1],O_WRONLY | O_CREAT,0666);
return 0;
}
2.write
第一个参数传文件表示符,第二个参数传一个指向要写入的数据的缓冲区的指针,第三个参数表示写入的大小
3.read
第一个参数是文件标识符,第二个参数是一个指针,指向用于存储读取数据的缓冲区,第三个参数是该区域大小。
4.close
5.文件描述符fd
open返回值,成功时返回一个文件描述符,失败返回-1
现在我们看一下文件的返回值是多少,有什么用?
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
//不让系统屏蔽某些权限
//umask(0);
//以只写方式打开,如果文件不存在则创建
int fd1 = open("log.txt",O_WRONLY | O_CREAT,0666);
if(fd1<0)
{
perror("open");
return 0;
}
printf("fd1: %d\n",fd1);
return 0;
}
此时文件标识符为3,为什么文件打开从3开始呢?
因为进程启动,默认打开了三个标准的输入输出流stdin,stdout,stderr,因为Linux下一切皆文件,这三个标准的输入输出流被当成文件打开了。
现在我们使用系统调用接口来进行文件写入,如下代码:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
//不让系统屏蔽某些权限
//umask(0);
//以只写方式打开,如果文件不存在则创建
int fd1 = open("log.txt",O_WRONLY | O_CREAT,0666);
if(fd1<0)
{
perror("open");
return 0;
}
printf("fd1: %d\n",fd1);
const char *message = "hello word\n";
write(fd1,message,strlen(message));
close(fd1);
return 0;
}
紧接着,我们只修改一个message指向的内容,原本文件内容不变:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
//不让系统屏蔽某些权限
//umask(0);
//以只写方式打开,如果文件不存在则创建
int fd1 = open("log.txt",O_WRONLY | O_CREAT,0666);
if(fd1<0)
{
perror("open");
return 0;
}
printf("fd1: %d\n",fd1);
const char *message = "aaaaaa";
write(fd1,message,strlen(message));
close(fd1);
return 0;
}
因为在做操作时只告诉了写入,并没有告诉要清空,只覆盖在原来基础上进行覆盖式的写入!!!
所以我们再加个选项O_TRUNC,如果文件存在则进行先清空:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
//不让系统屏蔽某些权限
//umask(0);
//以只写方式打开,如果文件不存在则创建,如果存在则清空
int fd1 = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
if(fd1<0)
{
perror("open");
return 0;
}
printf("fd1: %d\n",fd1);
const char *message = "aaaaaa\n";
write(fd1,message,strlen(message));
close(fd1);
return 0;
}
如果对该文件只进行了打开然后关闭不做写入:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
//不让系统屏蔽某些权限
//umask(0);
//以只写方式打开,如果文件不存在则创建,如果存在则清空
int fd1 = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
if(fd1<0)
{
perror("open");
return 0;
}
printf("fd1: %d\n",fd1);
const char *message = "aaaaaa\n";
//write(fd1,message,strlen(message));
close(fd1);
return 0;
}
内容被清空了,这样O_WRONLY | O_CREAT | O_TRUNC传参,最终作用就是和用fopen打开使用"w"方法一样,fopen使用"w"方法底层就是封装了这!!!
现在我们使用追加的形式进行写入:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
//不让系统屏蔽某些权限
//umask(0);
//以只写方式打开,如果文件不存在则创建,如果存在则清空
//int fd1 = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd1 = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
if(fd1<0)
{
perror("open");
return 0;
}
printf("fd1: %d\n",fd1);
const char *message = "aaaaaa\n";
write(fd1,message,strlen(message));
close(fd1);
return 0;
}
在上一个log.txt基础上进行内容的追加。
这样O_WRONLY | O_CREAT | O_APPEND传参,最终作用就是和用fopen打开使用"a"方法一样,fopen使用"a"方法底层就是封装了这!!!
fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数 (libc)。
open close read write lseek 都属于系统提供的接⼝,称之为系统调⽤接⼝
fopen fclose fread fwrite底层就是对open close read write进行了封装。
关于fd的问题:
我们连续打开几个文件,看看文件描述符是多少?
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
//不让系统屏蔽某些权限
//umask(0);
//以只写方式打开,如果文件不存在则创建,如果存在则清空
//int fd1 = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
int fd1 = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
int fd2 = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
int fd3 = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
int fd4 = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
if(fd1<0)
{
perror("open");
return 0;
}
printf("fd1: %d\n",fd1);
printf("fd2: %d\n",fd2);
printf("fd3: %d\n",fd3);
printf("fd4: %d\n",fd4);
const char *message = "aaaaaa\n";
write(fd1,message,strlen(message));
close(fd1);
return 0;
}
观察到文件描述符从3开始依次创建!!!
而前面我们说到,0,1,2被键盘,显示器,显示器占用,又说过,这些硬件在底层被包装成文件的形式,同样,我们能不能通过0,1,2进行对键盘文件,显示器文件进行写与读呢?
看下面代码:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
const char *message = "hello write\n";
write(1,message,strlen(message));
return 0;
}
我们通过write,直接向文件描述符1,进行写入message指向的内容,结果的确打印在屏幕上
我们再来看看下面调用read进行读:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
//abcd
char buffer[128];
ssize_t s = read(0,buffer,sizeof(buffer));
if(s > 0)
{
//把回车改为0
buffer[s-1] = 0;
printf("%s\n",buffer);
}
return 0;
}
我们键盘输入abcd,然后打印,发现的确从标识符0进行读取。
内核角度:
我们来从内核角度进行观察,当我们磁盘中有个文件log.txt包含内容+属性,当我们把程序进行加载,就变成了进程,OS为了对其进行管理,就有了PCB,CPU调度该进程,执行到open函数,此时打开文件,也就是把磁盘中文件加载到内存,加载到内存中,那么OS也要对其进行管理,先描述再组织,OS要为我们文件创建一个数据结构struct file,里面包含文件的属性,一个文件就对应一个struct内核对象,而进程启动时,会默认打开三个输入输出的文件,当我们打开了好几个文件,创建这些文件的struct file,然后为了方便管理,struct file里面包含一个struct file*的指针,通过这个指针就可以把这些文件以链表的形式进行管理起来,文件链表的形式。每创建一个文件,就链入该链表中进行管理,所以这样把对文件的管理,转换成对链表的增删查改,
而Linux内核以软件的一贯特性,进程管理和文件管理,这是两个单元的,他们两个不能是强耦合,必须是松耦合的!!!
所以我们现在就是要把进程和文件进行关联起来,我们进程是一个结构体对象,文件也是一个结构体对象,这就可以看成结构体与结构体直接的联系,一般情况下一个进程可以打开多个文件,1:n的形式
所以接下来,在我们task_struct中存在一个struct file_struct *files的结构,所指向的struct file_strct结构体对象里面存在一个很重要的成员变量struct file* fd_array[]。包含了一个指针数组。天然就具备下标0 1 2 3.....。然后把键盘文件,显示器文件,显示器文件填入进来,0 1 2就被他们三个给占用了后来我们调用接口open,打开文件,然后我们系统在查的时候在这个数组里面查0 1 2都被占用,3没有被占,然后加载打开这个文件,在内核中把这个struct file创建出来,把该结构体对象的地址填入到3号下标位置。
所以writr(3,message,strlen(message)),就是进程拿着3号描述符找到自己进程内部的
struct file_struct *files,然后找到struct file *fd_arryay[]然后找到3号下标对应的文件进行写入。
这个表叫做文件描述符表,这个表是构建了进程和文件之间的关系的一张表,每一个进程都有这一张表,双方通过指针来完成各个模块的关联和解耦!!!!
我们来打开内核源代码来看看到底是不是这回事
所以fd到底是什么?-----数组下标!!!
在系统里面 ,fd文件描述符是访问文件的唯一方式!!!
可是我们在C语言里用的全是文件流FILE *,而不是fd文件描述符?
所以什么是FILE?什么是FILE*?
这个是C语言给我们提供访问文件的一个东西,因为只能通过文件描述符进行访问,这个FILE是在C语言上封装的一个struct FILE,结构体对象,这个结构体里必定要有很多的属性,但这个属性里面肯定有一个对文件描述符进行封装的一个成员变量!!!
我们来看一下下面代码来进行验证:因为stdin,stdout,stderr是FILE*类型,所以用他进行指向一个成员变量_fileno,就可以看到文件描述符:
cpp
int main()
{
printf("stdin:%d\n",stdin->_fileno);
printf("stdout:%d\n",stdout->_fileno);
printf("stderr:%d\n",stderr->_fileno);
return 0;
}
再来fopen打开一个文件:
cpp
int main()
{
printf("stdin:%d\n",stdin->_fileno);
printf("stdout:%d\n",stdout->_fileno);
printf("stderr:%d\n",stderr->_fileno);
FILE *fp = fopen("log.txt","w");
printf("fp:%d\n",fp->_fileno);
return 0;
}
这样就验证上述说法!!!
所以任你文件怎么办,OS只认文件描述符!!!
在C语言上,用到的函数都是对系统调用的封装!!!
不仅做了接口上的封装,还做了类型上的封装就是struct_file!!!
重新理解一切皆文件:
如果理解硬件为文件呢?
OS被称为软硬件的管理者,我们OS系统不光对软件进行管理,也要对硬件进行管理,先描述再组织,所以我们硬件也一定要是一个结构体。
在内核中肯定要有描述这些设备,每一个设备都有一个对应的strut device结构体对象,然后OS通过链表对这些结构体进行管理起来,对设备的管理就变成 对链表的增删查改,对于外设,属性类别一样,但是值可以不一样,对于这些外设最核心的动作就是IO,读写,每一种设备都要有查看内容的放入,最核心的方法就是读和写一个read方法和write方法。
输出一个结论,各个设备的属性可以统一设置成结构体,来体现设备的差异,但方法同样也是不一样的,虽然都叫读写。
对于外设有读写方法,比如键盘读写,显然读写方法没有,就可以把写方法设置为空。
所有的方法在底层实现上肯定是不一样的。
虽然他们方法底层实现不一样,所以Linux设计者,设计,当我们OS启动的时候,创建我们
struct file对象,我们struct file里面有,C语言结构体里面不能包含方法,但是可以包含函数指针:
我们可以让读指针指向底层的方法,写指针指向底层的方法,我们所对应的设备,在在内核中都创建对应的struct file,然后每个对应设备的struct file里面的函数指针指向对应的方法,站在struct file角度,我们要进行读写,我们压根就不需要关系底层实现方法,只要知道函数指针,就能找到底层方法!!!向上我们要访问任何一个硬件,统一叫做读和写等方法,不用关注底层实现,只需要调这个方法,他自动给我们找到底层实现方法。
struct file就想当于在软件层面进行了一次封装,在上层看来,一切皆文件,对上层来说只需要提供struct file就可以。只要找到struct file就可以找到底层的方法,这套机制在Linux中被叫做VFS(virtual file system)(虚拟文件系统)。
这样在上层就看不到各个设备的差异了,所以一切皆文件!!!
所有用户的行为都会被转换成进程,无论启动读写等各种命令,所有行为都是进程,而我们站在进程角度,只需要拿文件描述符,找到对应的struct file,剩下的就不是进程的事情了,只要找到执行file中的方法,就可以完成对这些设备的操作。
所以进程角度,一起皆文件。
所以之前用的系统调用函数open,read,write,访问键盘显示器,都是调用底层设备对应
struct file中的函数指针对应的方法进行访问。
在上层一切皆文件,底层有各种设备,这种语系在C++当中,这种技术叫做多态!!!这也是C语言实现多态的方法!!!
补充:底层实现的这些方法全是在驱动程序里面!!!
打开内核源代码看看:
文本写入VS二进程写入
在计算机里,OS系统层面上只有二进制概念,语言层看起来可以文本写入,也可以二进制写入。
为什么我们语言喜欢做封装?(C/C++)
我们向显示器写12345,我写的是12345这个整数呢,还是'1''2''3''4''5'字符?-----字符!
我们显示器我们给他叫做字符设备,有一个东西叫做ACSLL码表,写12345,实际上是这几个字符ACSLL码值二进制,这个二进程被我们显示器解释成12345字符
看下面两段代码:
cpp
int main()
{
char *message = "hello\n";
write(1,message,strlen(message));
return 0;
}
cpp
int main()
{
int a = 12345;
write(1,&a,sizeof(a));
printf("\n");
return 0;
}
把字符显示到显示器上,直接就是字符,把数字输入到显示器上,显示器是字符设备只认字符,只不过这样的字符转成我们对应的二进程,被显示器转换成字符'9''0'。
所以我们在显示到显示器之前,我们给转换成字符,然后再进行显示,如下代码所示:
cpp
int main()
{
int a = 12345;
char buffer[1024];
snprintf(buffer,strlen(buffer),"%d",a);
write(1,buffer,strlen(buffer));
printf("\n");
return 0;
}
这样才能把12345显示到显示器上 ,所以用系统调用是没办法直接把一个整数答应到显示器上,必须进行相关的转换,
所以我们为什么要有printf相关这样的函数呢?不是有write这样的接口呢?、
因为我们再很多的情况,我们需要把内存级的二进制数据转换成字符风格,通过write打印在显示器上,这个过程叫做格式化的过程。
如果在系统中只有系统调用,那么这个格式化的过程必须要我们手动自己设置,然后才能显示。
所以C语言提供一些printf,scanf等一系列接口函数,直接可以进行使用打印,因为在底层对格式化的过程进行了封装!!!
所以为什么我们C语言要给我们很多接口做封装?
1.方便用户进行操作
2.提高语言的可移植性
系统调用的接口类型设计成void*,所以 传过来字符串或者其他类型,你以为你传的是这些类型,在这些接口看来是二进制,因为是void*。
读到的数据,写到的数据,做转化,都是用户进行操作,然后C语言对其进行封装,用户就能直接使用。
为什么喜欢做封装?如果不是Linux平台,而是win或者macos平台?
如果是系统调用接口,那么换平台就不能运行的,如果是封装的接口,在Linux调Linux的系统调用接口,在win调win的接口,在什么平台就调什么平台的接口。
所以语言层,把我们与平台强相关的接口操作做封装,提高语言的可移植性。
在使用这些接口的时候,win和Linux或者其他平台提前给我们安装了一些东西,这个东西叫glibc的库,语言层使用的一些接口,在glibc中进行了封装,把库编成Linux版本的库,win版本的库等等
语言的可移植性性越高,这个语言就越流通!!!!