函数原型:
FILE *fopen(const char *path, const char *mode);
参数:
参数 | 打开方式 |
---|---|
r | 只读,文件流指向文件头部 |
r+ | 读写,文件流指向文件的头部 |
w | 只写,如果文件存在,则清空文件开始写,如果文件不存在,则创建文件 |
w+ | 读写,如果文件存在,则清空文件开始写,如果文件不存在,则创建文件 |
a | 追加写,如果文件不存在,则创建文件,从文件末尾开始写 |
a+ | 可读也可追+加写,如果文件不存在,则创建文件,从文件末尾开始写 |
有+号的,都可以进行读。
返回值:
成功返回文件流指针,失败返回NULL。
函数原型:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
功能就是用来写文件的。
参数:
返回值:
返回成功写入到文件当中的块的数量,不是成功写入的字节,而是成功写入的块的个数。
代码验证:
#include <stdio.h>
2 #include <string.h>
3 int main(){
4 FILE *fp = fopen("./b.txt","w+");
5 if(fp==NULL){
6 perror("fopen");
7 return 0;
8 }
9 const char* str = "hello world";
10 ssize_t w_size = fwrite(str,1,strlen(str),fp);
11 printf("w_size:%d\n",w_size);
12 return 0;
13 }
通常设置块的大小为一个字节,那么块的个数参数就可以按照字符串的字节数量进行填充。返回值就相当于是成功写入的字节数量,因为一个块的大小为1。
函数原型:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数:
返回值:
成功读入的文件块的个数
代码验证:
#include <stdio.h>
2 int main(){
3 FILE *fp = fopen("./b.txt","r");
4 if(fp == NULL){
5 perror("fopen");
6 return 0;
7 }
8
9 char buf[1024] = {0};
10 ssize_t r_size = fread(buf,1,sizeof(buf)-1,fp);
11 printf("r_size : %d,buf : %s\n",r_size,buf);
12 return 0;
13 }
ssize_t r_size = fread(buf,1,sizeof(buf)-1,fp);
这个地方为什么是“sizeof(buf)-1”呢?因为我们要预留“\0”的位置,假设我们有三个字节来存放读到的内容,我们当然可以把三个空间都放读到的内容,不会出错,那为什么还要预留“\0”的位置呢?因为不访问这块空间还不会出事儿,一旦访问,就很可能会因为没有结束标志导致程序崩溃。为了保证安全性,防止越界访问造成程序的崩溃,我们预留一个\0的位置。
函数原型:
int fseek(FILE *stream, long offset, int whence);
参数:
宏 | 含义 |
---|---|
SEEK_SET | 文件头部 |
SEEK_CUR | 当前文件流指针的位置 |
SEEK_END | 文件末尾 |
返回值:
成功返回0,失败返回-1.
fseek函数是为了移动文件流指针产生的,那在什么情况下需要我们去移动文件流指针呢?我们来看看下面这段代码:
#include <stdio.h>
2 #include <string.h>
3 int main(){
4 FILE *fp = fopen("./b.txt","w+");
5 if(fp==NULL){
6 perror("fopen");
7 return 0;
8 }
9
10 const char* str = "hello world";
11 ssize_t w_size = fwrite(str,1,strlen(str),fp);
W> 12 printf("w_size:%d\n",w_size);
13
14 char buf[1024] = {0};
15 ssize_t r_size = fread(buf,1,sizeof(buf) - 1,fp);
W> 16 printf("r_size : %d,buf : %s\n",r_size,buf);
17 return 0;
18 }
按理说,我们先往文件当中写,再从文件当中读,没什么问题,但是上述代码的执行结果并不是我们想要的。
执行结果:
这是为什么呢?因为我们往文件当中写完内容之后,文件流指针是指向最后的,我们读取的时候,那从最后去读肯定读不到内容啦。于是我们就需要fseek函数去移动文件流指针。
我们通过fseek函数去修改一下上述代码的文件流指针,然后再次打印试试:
#include <stdio.h>
2 #include <string.h>
3 int main(){
4 FILE *fp = fopen("./b.txt","w+");
5 if(fp==NULL){
6 perror("fopen");
7 return 0;
8 }
9
10 const char* str = "hello world";
11 ssize_t w_size = fwrite(str,1,strlen(str),fp);
W> 12 printf("w_size:%d\n",w_size);
13
14 fseek(fp,6,SEEK_SET);
15
16 char buf[1024] = {0};
17 ssize_t r_size = fread(buf,1,sizeof(buf) - 1,fp);
W> 18 printf("r_size : %d,buf : %s\n",r_size,buf);
19 return 0;
20 }
~
现在就可以正常打印了。
函数原型:
int fclose(FILE *fp);
打开文件之后,一定记得关闭文件,否则容易造成文件句柄泄漏,也就是内存的泄漏。
函数原型:
int open(const char *pathname, int flags, mode_t mode);
参数:
宏 | 含义 |
---|---|
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读、写打开 |
上面这三个常量,必须指定一个且只能指定一个。
宏 | 含义 |
---|---|
O_CREAT | 若文件不存在,则创建它,需要使用mode选项,来指明新文件的访问权限 |
O_APPEND | 追加写 |
上面这两个可以多选,与前三个用或连接。
返回值:
成功则返回新打开的文件描述符,失败返回-1.
我们说在进程创建之初,就默认打开了三个文件,标准输入,标准输出,标准错误。标准输入在C库当中是stdin,在操作系统内核进程部分就是0号文件描述符。
函数原型:
ssize_t write(int fd, const void *buf, size_t count);
参数:
返回值:
返回则写入的字节数量
函数原型:
ssize_t read(int fd, void *buf, size_t count);
参数;
返回值:
成功则返回读到的字节数量
函数原型:
off_t lseek(int fd, off_t offset, int whence);
参数:
宏 | 含义 |
---|---|
SEEK_SET | 文件头部 |
SEEK_CUR | 当前文件流指针的位置 |
SEEK_END | 文件末尾 |
返回值:
成功返回偏移的位置,单位是字节。失败返回-1.
函数原型:
int close(int fd);
函数功能:关闭文件描述符。
代码验证上述函数:
#include <stdio.h>
2 #include <fcntl.h>
3 #include <string.h>
4 #include <unistd.h>
5 int main(){
6 //打开一个文件
7 int fd = open("./c.txt",O_RDWR | O_CREAT,0664);
8 if(fd == -1){
9 perror("open");
10 return 0;
11 }
12
13 //往文件中写
14 const char* str = "happyday";
15 size_t w_size = write(fd,str,strlen(str));
W> 16 printf("w_size : %d\n",w_size);
17
18 //偏移一下
19 lseek(fd,0,SEEK_SET);
20
21 //从文件中往外读
22 char buf[1024] = {0};
23 size_t r_size = read(fd,buf,sizeof(buf) - 1);
W> 24 printf("r_size :%d,buf : %s\n",r_size,buf);
25
26 printf("fd : %d\n",fd);
27 //关闭文件
28 close(fd);
29 return 0;
30 }
执行结果:
文件描述符的值是一个小正数。
我们循环打印文件描述符:
1 #include <stdio.h>
2 #include <fcntl.h>
3 int main(){
4 while(1){
5 int fd = open("./c.txt",O_RDWR | O_CREAT,0664);
6 if(fd<0){
7 perror("open");
8 return 0;
9 }
10 printf("fd : %d\n",fd);
11 }
12 return 0;
13 }
执行结果:
循环了十万次。
为什么是十万次呢?这是操作系统的一个软限制,一个进程打开的文件太多了,达到了一个进程打开文件描述符的上限了。
软限制是可以修改的,但是因为硬限制的存在,这个打开个数也并不能无限增加,那硬限制是什么呢?硬限制是操作系统的资源,因为打开文件描述符是需要耗费内存资源的。
可以在/proc/[pid]/fd文件夹下查看文件描述符信息,观察到文件描述符的分配规则是最小未使用原则。
文件是在进程当中打开的,所以文件描述符和进程脱不了关系,在进程的结构体中,有一个描述进程打开文件信息的结构体指针,该指针指向一个结构体,这个结构体有一个指针数组,数组里面存储的指针指向描述文件信息的结构体,而我们所说的文件描述符就是该指针数组的数组元素下标。在描述文件信息的结构体里,有描述文件的名称、文件的大小、文件的权限、文件的所有者、文件的属性、文件在磁盘当中存储的位置等等。
typedef struct _IO_FILE FILE;
从源码的角度来说,文件流指针是一个结构体(struct _IO_FILE),文件流指针对应的结构体是C库定义的,该结构体属于C库的内容,它并不是操作系统内核的东西,文件流指针结构体内的读缓冲区和写缓冲区也是属于C库,不属于操作系统内核的,所以我们在进程控制当中说进程结束的时候会刷新缓冲区,需要用那四种方法去刷新缓冲区。当调用_exit函数去终止进程的时候,会比exit函数少做的其中一件事就是刷新缓冲区,因为_exit函数是系统调用函数,在内核中运行,接触不到缓冲区。
文件流指针结构体当中还有一个东西是int _fileno,该整形变量用来保存文件描述符的数值,保存的就是内核的文件描述符。