我之前是讲过c语言的文件操作的,但是说实话我压根就不知道它在干什么,后面c语言/c++,数据结构的学习过程中也没用过文件操作,今天我们就来会会这个文件操作
1.回顾c语言文件接口
1.1.fopen
- r :只读模式打开,文件流指针指向文件的起始位置
- r+:可读可写方式打开,文件流指针指向文件的起始位置
- w:只写模式打开,被打开文件若存在,则截断文件(清空文件内容),若不存在,创建该文件
- w+:可读可写模式打开,,被打开文件若存在,则截断文件(清空文件内容),若不存在,创建该文件
- a:追加写。若文件存在,文件流指针指向文件的末尾进行写。若不存在,创建文件
- a+:可以读也可以 追加写。文件存在,读的位置被初始化到文件头,追加写的时候依旧是在文件末尾追加 ,文件不存在,创建该文件
我们来实验一下
1.1.1.r选项
#include <stdio.h> int main() { FILE* fp = fopen("log.txt", "r"); if (fp == NULL){ perror("fopen"); return 1; } char buffer[64]; for (int i = 0; i < 5; i++){ fgets(buffer, sizeof(buffer), fp);//读取 printf("%s", buffer); } fclose(fp); return 0; }
我们发现只是单纯的读取而已
1.1.2.w选项
#include <stdio.h> int main() { FILE* fp = fopen("log.txt", "w"); if (fp == NULL){ perror("fopen"); return 1; } int count = 5; while (count){ fputs("hello world\n", fp);//向文件中写入 count--; } fclose(fp); return 0; }
运行后,当前路径下会生成log.txt文件,并会写入内容。
当我们以“w”的方式打开文件代表写入,此时他会清空文件,再帮我们写入。
1.1.3.其他
当我们把选项换成“a”,这就代表append追加,这就不会清空文件。
再命令行中的“>”符号,这叫做输出重定向。
可以看到之前的内容就没有了,变成了重定向的内容,他也是类似于“w”的方式。
再来看一下“r”选项打开文件,可以读取文件中的数据到缓冲区中,并输出到屏幕上。
int main() { FILE* fp = fopen("log.txt", "r"); if (fp == NULL) { perror("fopen"); return 1; } // 文件操作 char line[64];// 缓冲区 // fgets是C语言的接口,按行读取,自动在字符结尾添加\0 while (fgets(line, sizeof(line), fp) != NULL) { fprintf(stdout, "%s", line); } fclose(fp); return 0; }
加入命令行参数后就变成了类似cat命令的操作,也可以给简易的shell添加上这个功能。
int main(int argc, char* argv[]) { if (argc != 2) { printf("请输入两个参数:程序+文件名\n"); exit(1); } FILE* fp = fopen(argv[1], "r"); if (fp == NULL) { perror("fopen"); return 1; } // 文件操作 char line[64];// 缓冲区 // fgets是C语言的接口,按行读取,自动在字符结尾添加\0 while (fgets(line, sizeof(line), fp) != NULL) { fprintf(stdout, "%s", line); } fclose(fp); return 0; }
2.默认打开的三个流(stdin,stdout,stderr)
Linux下一切皆文件,显示器和键盘也可以看作文件。
我们能看到显示器上的数据,是因为我们向显示器写入了数据,电脑能获取我们键盘上的字符,是电脑从“键盘文件” 读取了数据。
为什么我们向“显示器文件”写入数据以及从“键盘文件”读取数据前,不需要进行打开“显示器文件”和“键盘文件”的相应操作?
需要注意的是,打开文件一定是进程运行的时候打开的,而任何进程在运行的时候都会默认打开三个输入输出流,即标准输入流、标准输出流以及标准错误流,对应到C语言当中就是stdin、stdout以及stderr。
其中,标准输入流对应的设备就是键盘,标准输出流和标准错误流对应的设备都是显示器。
查看man手册我们就可以发现,stdin、stdout以及stderr这三个家伙实际上都是FILE*类型的。
stdin默认为键盘,stdout和stderr默认为显示器,这些都是硬件,但在Linux下,一切皆文件,所以这三个的类型都是FILE*,这是个指针,FILE其实是C标准库提供的结构体
当我们的程序被运行起来时,操作系统就会默认使用C语言的相关接口将这三个输入输出流打开。
注意: 不止是C语言当中有标准输入流、标准输出流和标准错误流,C++当中也有对应的cin、cout和cerr,其他所有语言当中都有类似的概念。实际上这种特性并不是某种语言所特有的,而是由操作系统所支持的。
3.系统文件IO
通过之前的学习,这些文件操作最终都是访问硬件(显示器、键盘、磁盘)。众所周知,OS是硬件的管理者。所有语言上对“文件”的操作,都必须贯穿操作系统。然而OS不相信任何人,访问操作系统,就必须要通过系统接口!!
open/fclose,fread/fwrite,fputs/fgets,fgets/fputs 等库函数一定需要使用OS提供的系统调用接口,接下来我们就来学习文件的系统调用接口,才能做到万变不离其宗!!
我们在Linux平台下运行C代码时,C库函数就是对Linux系统调用接口进行的封装,在Windows平台下运行C代码时,C库函数就是对Windows系统调用接口进行的封装,这样做使得语言有了跨平台性,也方便进行二次开发。
3.1.open
3.1.1.参数pathname
open的第一个参数:pathname
open的第一个参数表示打开或者创建目标文件。
在这里要注意的是:
- 1.如果以路径的形式给出那么当需要创建文件的时候,会在你提供的这个路径下创建。
- 2.如果只给了文件名,那么会在当前路径下创建(当前路径上面以及提过注意他的含义)。
3.1.2.参数flags
open的第二个参数:flags
open的第二个参数表示文件打开的方式。常用选项有如下几种:
- O_RDONLY:只读打开
- O_WRONLY:只写打开
- O_RDWR:读,写打开
上面三个常量,必须指定一个且只能指定一个
- O_CREAT:若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
- O_APPEND:追加写
- O_TRUNC:清空文件
我们在打开文件时可以使用多个选项中间以|隔开。
举个例子:若想以只写的方式打开文件,但当目标文件不存在时自动创建文件,则第二个参数设置如下:
O_WRONLY|O_CREAT
那flags到底是个什么呢?
其实它就是一个整数,一个整数有32个比特位,将每一个比特位做为某一个选项,在对应的函数内看哪一位是否有数字,来判断我们是否传入了这个选项,理论上flags可以传递32种不同的标志位。
大多数实现都把O_RDONLY定义为0,把O_WRONLY定义为1,把O_RDWR定义为2
按照上面的说法就是意味着O_WRONLY对应的是32个比特位中只有最低位是1,到底是不是这样?
下面我们使用vim打开/usr/include/asm-generic/fcntl.h这个目录下的文件看一看:
有人就有疑问了,这怎么还出现2,3了啊?说好的二进制呢??
但事实上这个写法是十六进制,,4位二进制表示一个16,刚好32位二进制就能换成8位十六进制了
事实确实如此
这些宏定义选项的共同点就是,它们的二进制序列当中有且只有一个比特位是1(O_RDONLY选项的二进制序列为全0,表示O_RDONLY选项为默认选项),且为1的比特位是各不相同的,这样一来,在open函数内部就可以通过使用“与”运算来判断是否设置了某一选项。
而在open函数中使用特定的数字进行判断然后只写具体的功能。
那第二个参数flags(int)是怎么实现模式的叠加的?为什么要把模式|在一起呢?
这是一种用户层给内核传递标志位的常用做法。
int有32个比特位,不重复的一个bit,就可以表示不同状态,就可以传递多个标志位且位运算效率较高。下面用一段代码演示一下。
// 用int中不同的bit位就可以标识一种值 #define ONE 0x1 // 0000 0001 #define TWO 0x2 // 0000 0010 #define THREE 0x4 // 0000 0100 void Print(int flags) { if (flags & ONE) printf("one\n"); if (flags & TWO) printf("two\n"); if (flags & THREE) printf("three\n"); } int main() { Print(ONE | TWO); // 0001 | 0010 -> 0011 printf("--------------\n"); Print(ONE | TWO | THREE); // 0001 | 0010 | 0100 -> 0111 return 0; }
相信看到这里,你也就明白了为什么模式可以通过|来叠加的?
3.1.3.参数mode
(注意当不创建文件时,第三个参数可以不用填)
open的第三个参数:第三个参数为设置创建文件的权限。
在linux中文件是有权限的,当以只写方式打开文件,文件如果不存在需要创建但是创建时我们需要设置文件的权限。
3.1.4.返回值
open的返回值是指我们打开文件的这个文件描述符,打开失败返回-1。
下面我们通过一段代码进行演示:
#include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> int main() { umask(0); int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666); int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666); int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666); int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666); int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666); printf("fd1:%d\n", fd1); printf("fd2:%d\n", fd2); printf("fd3:%d\n", fd3); printf("fd4:%d\n", fd4); printf("fd5:%d\n", fd5); return 0; }
我们可以发现,文件的文件描述符是从3后面递增的。
实际上这里所谓的文件描述符本质上是一个指针数组的下标,指针数组当中的每一个指针都指向一个被打开文件的文件信息,通过对应文件的文件描述符就可以找到对应的文件信息。
当使用open函数打开文件成功时数组当中的指针个数增加,然后将该指针在数组当中的下标进行返回,而当文件打开失败时直接返回-1,因此,成功打开多个文件时所获得的文件描述符就是连续且递增的。
而Linux进程默认情况下会有3个缺省打开的文件描述符,分别就是标准输入0、标准输出1、标准错误2,这就是为什么成功打开文件时所得到的文件描述符是从3开始进程分配的。
3.1.5.使用示例
#include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { int fd = open("log.txt", O_WRONLY);//以只读方式打开文件 if (fd < 0) { perror("open"); exit(1); } // 打开成功 printf("fd: %d\n", fd); return 0; }
我们发现,没有log.txt的情况下,只读模式情况下打印了这个
我们换换只写模式
还是不会自己创建啊,看来这个写和c语言的w模式还是有点区别的
我们往上看第二个参数,发现创建文件还是要加上一个选项的
我们修改一下代码
通过这次实验,完美发现c语言的那些r,w等模式不就是open的这些模式的混合版本嘛!!
事实上确实是这样,不只是c语言,包括c++,python等都是如此
这次是创建成功了,但是它的权限好奇怪啊
我们不要忘了open它有第三个参数mode,这就是创建文件的访问权限,想要给他的权限设置为0666,就要把它传入,当然也要注意umask,系统的默认umask是0002,所以还要在一开始设置当前进程的umask,如何设置也可以用接口。
这个权限非常完美了吧!!!
3.2.creat
这个函数是用来创建一个新文件的
#include<fcntl.h> int creat(const char*path,mode_t mode);
注意: 这个函数和下面这个等效
open (path, O_WRONLY | O_CREAT | O_TRUNC, mode);
在早期的UNIX系统版本中,open的第二个参数只能是0、1或2。无法打开一个尚未存在的文件,因此需要另一个系统调用creat以创建新文件。现在,open函数提供了选项O_CREAF和OTRUNC,于是也就不再需要单独的creat 函数。
creat的一个不足之处是它以只写方式打开所创建的文件。
在提供open的新版本之前,如果要创建一个临时文件,并要先写该文件,然后又读该文件,则必须先调用creat、close,然后再调用open。
现在则可用下列方式调用open实现:
open (path, O_RDWR | O_CREAT | O_TRUNC, mode);
3.2.close
这个函数是用来关闭一个打开文件
使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。
3.3.write
系统接口中使用write函数向文件写入信息,write函数的函数原型如下:
write函数,将buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。
写入成功返回写入数据的字节个数,失败返回-1.
我们来使用一下
又一次写入时,我们发现:
O_TRUNC
: 打开文件的时候直接清空文件
O_TRUNC:
如果文件已经存在并且是一个常规文件,并且开放模式允许写入(即是0_RDNRor O_MwRONLY),那么它将被截断为长度为0(也就是清空文件)
这样就可以变成C语言中fopen的w选项。
这样a选项也就好说了,选项变成O_APPEND就行了。
O_APPEND
: 追加文件
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
注意注意:写入文件的过程中,不需要写入
\0
!因为\0是C语言层面上规定字符串的结束标志,而写入文件关心的是字符串的内容,文件和语言不要搞混了
3.4.read
- 第一个参数是文件对应的文件描述符
- 第二个参数是读取的内容放到这里
- 第三个参数读取几个字节,返回值为实际读取的字节数读取失败返回-1.
读文件的前提:文件已经存在,不涉及创建及权限的问题,那么用两个参数的open打开文件即可
未完待续……