前言:
- 在之前,我已经对进程的相关知识进行了详细的介绍。本期开始,我们将要学习的是关于 “基础I/O”的知识!!!
(一)C文件接口
首先,在正式进入本期主题之前,我先用C文件的接口带大家简单的回顾下,顺便带大家认识相关的接口函数等。
首先就是往文件里面进行写数据操作:
#include <stdio.h> #include <string.h> #define LOG "log.txt" int main() { FILE *fp = fopen(LOG , "w"); if(!fp) { perror("fopen"); return 1; } const char *msg = "hello bit!\n"; int count = 5; while(count--) { //fputs(msg,fp); fwrite(msg, strlen(msg), 1, fp); } fclose(fp); return 0; }
输出展示:
【解释说明】
- 上述代码简单的实现了打开文件、写入文本内容,并关闭文件。在系统中,提供了丰富的用于对文件进行操作的函数;
- 打开文件使用
fopen
函数以写入模式打开文件,返回一个文件指针fp;
- 如果文件打开失败,
perror
函数将输出错误信息到标准错误输出流。 - 其次使用
fwrite
函数将字符串msg
通过文件指针fp
写入文件(除了这个外还有很多)
首先就是读文件里面的数据操作:
#define LOG "log.txt" int main() { FILE *fp = fopen(LOG, "r"); if(!fp) { perror("fopen"); return -1; } //正常进行文件操作 while(1) { char line[128]; if(fgets(line, sizeof(line), fp) == NULL) break; else printf("%s", line); } fclose(fp); return 0; }
输出展示:
【解释说明】
- 同样的对于读文件操作,在系统内部也提供的很多的函数去进行实现!
如上,是我们之前学的文件相关操作。还有 fseek ftell rewind 的函数。
(二)系统文件I/O
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问.
先来直接以代码的形式,实现和上面一模一样的代码(写操作):
#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 fd = open("myfile", O_WRONLY|O_CREAT, 0644); if(fd < 0){ perror("open"); return 1; } int count = 5; const char *msg = "hello bit!\n"; int len = strlen(msg); while(count--){ write(fd, msg, len); //fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 //返回值:实际写了多少字节数据 } close(fd); return 0; }
- C++中对文件进行读操作可写成下述:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { int fd = open("myfile", O_RDONLY); if(fd < 0){ perror("open"); return 1; } const char *msg = "hello bit!\n"; char buf[1024]; while(1){ ssize_t s = read(fd, buf, strlen(msg));//类比write if(s > 0){ printf("%s", buf); } else{ break; } } close(fd); return 0; }
1、接口介绍
接下来,我首先带大家认识相关的接口。随后大家在来理解上述这两段代码!!
- 最重要的当然是 open() ,通过对它的学习,我们将极大程度的理解有些知识
【解释说明】
参数:
path
:要打开的文件的路径字符串。flags
:打开文件的标志,可以使用一或多个以下标志的位或运算符|
第一个参数很好理解,对于第二个参数大家可能存在些疑惑我在这里重点讲讲关于这个标志的问题。
💨 首先,引入一个问题:OS是如何让用户给自己传递标志位的呢?
接下来,我给大家写个简单的 demo 样例帮助大家理解上述逻辑:
#include <stdio.h> #define ONE 0x1 #define TWO 0x2 #define THREE 0x4 #define FOUR 0x8 #define FIVE 0x10 // 0000 0000 0000 0000 0000 0000 0000 0000 void Print(int flags) { if(flags & ONE) printf("hello 1\n"); //充当不同的行为 if(flags & TWO) printf("hello 2\n"); if(flags & THREE) printf("hello 3\n"); if(flags & FOUR) printf("hello 4\n"); if(flags & FIVE) printf("hello 5\n"); } int main() { printf("--------------------------\n"); Print(ONE); printf("--------------------------\n"); Print(TWO); printf("--------------------------\n"); Print(FOUR); printf("--------------------------\n"); Print(ONE|TWO); printf("--------------------------\n"); Print(ONE|TWO|THREE); printf("--------------------------\n"); Print(ONE|TWO|THREE|FOUR|FIVE); printf("--------------------------\n"); return 0; }
输出展示:
【解释说明】
- 上述代码展示了使用位运算来模拟标志位。在这个示例中,每个标志位都表示一种不同的行为,当相应的位被设置时,相关的行为会执行;
- 首先,定义了一组宏来表示不同的标志位,每个标志位都对应一个唯一的二进制位。例如,
ONE
的二进制表示是:0000 0000 0000 0000 0000 0000 0000 0001
,TWO
的二进制表示是:0000 0000 0000 0000 0000 0000 0000 0010
,依此类推。 - 接下来,定义了一个
Print
函数,它接受一个整数参数flags
,这个参数表示一组标志位的组合。在函数内部,使用位运算和按位与操作符&
来检查每个标志位是否被设置。
有了上述认识,大家在返回去看 open() 函数中的 flags参数,我想就很明显了。当我们在man手册中往下面翻的时候就会看到如下内容:
- 这一个个的选项就相当于宏,我们只需关于宏函数内部的意义即可!
2、代码示例
接下来,我们通过写代码的方式来进行理解!!
首先,有如下代码示例:
int main() { int fd = open(LOG,O_WRONLY); if(fd == -1) { printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno)); } else printf("fd: %d, errno: %d, errstring: %s\n", fd, errno, strerror(errno)); close(fd); return 0; }
输出展示:
【解释说明】
- 上述代码的主要目的是打开一个文件并检查是否成功打开。如果打开失败,它将输出打开文件描述符(
fd
)、错误代码(errno
)和错误字符串(strerror(errno)
); - 然而,需要注意的是,因为开始并没有提供
LOG这个文件,因此最后的输出肯定是发生报错。
解决方法就是在我们 open 的时候,再加上 【o_CREAT】这个标志:
输出展示:
然而,此时出现了一个奇怪的现象,我们通过查看得知 log.txt 文件的权限是处于乱码的情况:
因此,基于上述情况的出现。我们一般在用的时候一般不用 带两个参数的 open() ,而是用带三个参数的open() 函数
【解释说明】
- mode 是一个
mode_t
类型的参数,用于设置文件的权限; - 它指定了文件的访问权限位,并且只有在使用
O_CREAT
标志创建文件时才会生效; - 通常使用8进制表示权限位。例如,
0644
表示文件所有者具有读写权限,其他用户只有读权限。
基于上述情况,我们在后面加上相应的权限(我这里加的是0666):
输出展示:
【解释说明】
- 请注意,文件权限可能会受到操作系统的默认配置或其他因素的影响,即 umask;
- 此时,系统默认的 umask 为2,因此在某些情况下,系统可能会对文件权限进行适当的修改,以使其符合操作系统的权限策略。
那么要如何保障这里的权限不受系统的影响呢?—— 我们可以自己设置
- 此时,我们又需要引入一个新的函数:
接下来,我们对代码进行修改操作:
输出展示:
【注意】
- open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
接下来,我们就需要往文件中写入数据了。在C++中,系统提供 write() 函数来进行写操作。
代码展示:
输出展示:
上述完成写入数据,接下来就是从文件中读出数据。在C++中,系统提供了 read() 函数
man手册查询:
代码展示:
输出展示:
【小结】
- 有了上述这些认识,在回过头去看我给出的 读和写 的代码就容易了!
(三)总结
上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc);
open close read write lseek 都属于系统提供的接口,称之为系统调用接口
回忆一下我们讲操作系统概念时,画的一张图:
- 根据上述图片,我们可以知道系统调用接口和库函数的关系,一目了然。
- 所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。