4. 标准 IO 库
- 1. 标准 IO 简介
- 2. FILE 指针
- 3. 标准输入、标准输出和标准错误
- 4. fopen() 和 flose()
- 5. fread() 和 fwrite()
- 6. fseek 定位
- 7. 检查或复位状态
- 7.1 feof()
- 7.2 ferrof()
- 7.3 clearerr()
- 8. 格式化 IO
- 8.1 格式化输出
- 8. 2 格式化输入
- 9. IO 缓冲
- 9.1 文件 IO 的内核缓冲
- 9.2 刷新文件 IO 的内核缓冲区
- 9.2.1 控制文件 IO 内核缓冲的系统调用
- 9.2.1 控制文件 IO 内核缓冲的标志
- 9.3 直接 IO,绕过内核缓冲
- 9.3.1 直接 IO 的对齐限制
- 9.3.2 直接 IO 与普通 IO 对比
- 9.4 stdio 缓冲
- 9.4.1 设置 stdio 缓冲
- 9.4.1.1 setvbuf()
- 9.4.1.2 setbuf()
- 9.4.1.3 setbuffer()
- 9.4.2 缓冲模式
- 9.4.3 刷新 stdio 缓冲区
- 10. 文件描述符和FILE指针互转
1. 标准 IO 简介
标准 IO 库指的式标准 C 库中用于文件 IO 操作相关的一系列库函数的集合,底层是用系统 IO 实现的。但是标准 IO 比系统 IO 具有更好的可移植性,因为不同的操作系统内核提供的系统调用都是不一样的;其次标准 IO 具有更高的效率,因为标准 IO 提供了自己的缓冲区,但是系统 IO 不具备缓冲区。
2. FILE 指针
FILE 指针就类似于系统 IO 的文件描述符,FILE 指针是一个结构体类型,包含了标准 IO 为管理文件所需要的所有信息,包括文件描述符、指向文件缓冲区的指针、缓冲区的长度,当前缓冲区中的字节数以及出错标志等。该类型定义在stdio.h
中
3. 标准输入、标准输出和标准错误
标准输入和标准输出通常指的就是键盘和显示器,标准错误也是通过显示器显示的。
通过标准输入设备与系统进行交互时,进程将从标准输入 (stdin) 文件中获取数据,将正常输出数据输出到标准输出 (stdout) 文件,将错误信息输出到标准错误 (stderr) 文件中。标准输出文件和标准错误文件都对应终端的屏幕,而标准输入文件则对应于键盘。
4. fopen() 和 flose()
#include <stdio.h>
int flose(FILE *stream);// 成功返回0,失败返回-1
FILE *fopen(const char *path, const cchar *mode);// 成功返回FILE文件指针,失败返回NULL
/* 参数:
* path: 文件路径,可以是相对路径,也可以是绝对路径
* mode: 文件权限,是一个字符串
* r:只读
* r+:可读可写
* w:只写,如果文件存在,就将文件截断为0,否则就创建文件
* w+:可读可写方式打开文件,如果存在就截断为0,否则就创建文件
* a:只写,默认是追加式写入,如果文件不存在就创建文件
* a+:可读可写,以追加的方式写入,如果文件不存在就创建文件
* /
// 如果文件不存在就会创建文件,新文件的默认权限是0666
5. fread() 和 fwrite()
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(void *ptr, size_t size, size_t nmemb, FILE *stream);
/* 参数:
* ptr:存放读取到的数据或者需要写入的数据的缓冲区
* size:读取或写入的数据大小的单位,那么总共数据的大小是size*nmemb
* nmemb:读取或写入的数据个数
* stream:FILE指针
*/
6. fseek 定位
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
// 这里的后两个参数和lseek相同,成功返回0,失败返回-1
long ftell(FILE *stream);
// 获取当前读写位置偏移量
7. 检查或复位状态
fread() 读取数据时,如果返回值小于 nmemb,表示发生了错误或者已经读到了文件末尾,但是不能具体确定是哪种情况,可以通过判断错误标志或 end-of-file 标志来确定具体情况
7.1 feof()
用于测试文件的 end-of-file 标志,如果被设置了,则调用 feof() 函数将返回一个非零值,如果没有被设置就返回0.
#include <stdio.h>
int feof(FILE *stream);
// 当文件的读写位置到结尾时,end-of-file 会被设置
7.2 ferrof()
用于测试文件的错误标志,如果错误标注被设置,返回非零值,否则返回 0.
#include <stdio.h>
int ferror(FILE *stream);
7.3 clearerr()
用于清除 end-of-file 或错误标志,当调用上面两个函数校验完之后,通常需要清除这些标志,避免下次校验时使用到的是上一次的值。对于 end-of-file,除了显示清除外,调用 fseek 会自动清除
#include <stdio.h>
void clearerr(FILE *stream);
8. 格式化 IO
8.1 格式化输出
#include <stdio.h>
int print(const char *format,...);
int fprintf(FILE *stream, const char *format,...);
int dprintf(int fd, const char *format,...);
int sprintf(char *buf, const char *format,...);
int snprintf(char *buf, size_t size, const char *format,...);
// format:格式化控制字符串,用于指定后续的参数如何进行格式转换
fprintf(stderr,"hello world\n");
fprintf(stderr,"%d\n",5);
dprintf(STDERR_FILENO,"hello world\n");
dprintf(STDERR_FILENO,"%d\n",5);
char buf[100];
sprintf(buf,"hello world\n");
sprintf(buf,"%d",100);// 将整形转换为字符串,并且自动在末尾加上一个终止符
// 因为sprinf可能会发生缓冲区溢出,就引入了snprintf
// size规定了缓冲区的大小,如果写入到缓冲区的字节数大于size,超出的部分就会丢弃
format:
%[flags][width][.precision][length]type
8. 2 格式化输入
#include <stdio.h>
int scanf(const char *format,...);
int fscanf(FILE *stream, const char *format,...);
int sscanf(const char *str, const char *format,...);
int a,b,c;
scanf("%d%d%d", &a&b&c);
// 使用该函数时,进程会被阻塞,直到键盘有数据输入
int a2,b2,c2;
fscanf(stdin,"%d%d%d",&a2,&b2,&c2);
char *str="5454 hello";
char buf[10];
int a3;
sscanf(str, "%d%s",&a,buf);
format:
%[*][width][ength]type
或%[m][width][ength]type
如果添加了 *,格式化输入函数会按照转换说明的指示读取输入,但是丢弃输入,意味着不需要对转换后的结果进行存储,所以也就不需要提供相应的指针参数。如果添加了 m,就只能与%s、 %c 以及%[一起使用,调用者无需分配相应的缓冲区来保存格式转换后的数据,原因在于添加了 m,这些格式化输入函数内部会自动分配足够大小的缓冲区,并将缓冲区的地址值通过与该格式转换相对应的指针参数返回出来,该指针参数应该是指向char*
变量的指针。随后,当不再需要此缓冲区时,调用者应调用 free() 函数来释放此缓冲区。
9. IO 缓冲
9.1 文件 IO 的内核缓冲
文件 IO 在进行文件读写操作时并不会直接访问磁盘设备,而是仅仅在用户空间缓冲区和内核缓冲区之间复制数据,也就是说系统调用和磁盘操作不是同步的。当多个线程同时向文件写入数据时,就会将文件存放在缓冲区中,然后只进行依次和磁盘的 IO 操作。
9.2 刷新文件 IO 的内核缓冲区
强制将文件 IO 的内核缓冲区中缓存的数据刷新到磁盘设备中
9.2.1 控制文件 IO 内核缓冲的系统调用
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void);// 不是对某个指定的文件数据进行更新,而是刷新所有文件 IO 内核缓冲区
9.2.1 控制文件 IO 内核缓冲的标志
fd=open(filepath,O_WRONLY|O_DSYNC); // 类似在每个write后调用fdatasync函数
fd=ofen(filepath,O_WRONLY|O_SYNC); // 每个write都会自动将文件内容数据和元数据刷新
9.3 直接 IO,绕过内核缓冲
fd=open(filepath,O_WRONLY|O_DIRECT);
9.3.1 直接 IO 的对齐限制
- 应用程序中用于存放数据的缓冲区,其内存起始地址必须以块大小的整数倍进行对齐
- 写文件时,文件的位置偏移量必须是块大小的整数倍
- 写入到文件的数据大小必须是块大小的整数倍
确认块大小指令df -h
查看 Ubuntu 系统的跟文件系统所挂载的磁盘分区,接着sudo tune2fs -l /dev/sda1 | grep "Block size"
9.3.2 直接 IO 与普通 IO 对比
直接 IO 每次都是直接对磁盘发起操作,而普通方式只是将用户空间下的数据拷贝到文件 IO 内核缓冲区中。直接 IO 效率、性能低,只有一些特殊场合用到
9.4 stdio 缓冲
标准 IO 效率比 文件 IO 高的根本就是它维护了自己的缓冲区,减少了和磁盘的交互
9.4.1 设置 stdio 缓冲
9.4.1.1 setvbuf()
#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
// 如果 buf 不为 NULL,那么buf指向size大小的内存区域将作为该文件的stdio缓冲区,所以buf应该以动态分配或静态的方式在堆上开辟空间,而不是在栈上的函数内分配局部变量。如果buf为NULL,那么stdio库会自动分配一块空间作为该文件的stdio缓冲区,除非mode配置为非缓冲区模式
/* mode:指定缓冲区的缓冲类型
* _IONBF:不对 IO 进行缓冲。每个标准IO函数将立即调用write或read,并且忽略buf和size参数,可以指定为NULL和0,stderr就是这类
* _IOLBF:采用行缓冲,遇到换行符才会执行文件IO操作。对于输出流,在输出一个换行符前将数据缓存(除非缓冲区已经被填满), 当输出换行符时,再将这一行数据通过文件 I/O write()函数刷入到内核缓冲区中;对于输入流, 每次读取一行数据。 对于终端设备默认采用的就是行缓冲模式,譬如标准输入和标准输出。
* _IOFBF: 采用全缓冲 I/O。 在这种情况下,在填满 stdio 缓冲区后才进行文件 I/O 操作(read、 write)。对于输出流,当 fwrite 写入文件的数据填满缓冲区时,才调用 write()将 stdio 缓冲区中的数据刷入内核缓冲区;对于输入流, 每次读取 stdio 缓冲区大小个字节数据。 默认普通磁盘上的常规文件默认常用这种缓冲模式
*/
// size指定缓冲区大小
当 stdio 缓冲区中的数据被刷入到内核缓冲区或被读取之后,这些数据就不会存在于缓冲区中了,数据被刷入了内核缓冲区或被读走了
9.4.1.2 setbuf()
#include <stdio.h>
void setbuf(FILE *stream, char *buf);
执行和 setvbuf 类似的任务
9.4.1.3 setbuffer()
#include <stdio.h>
void setbuffer(FILE *stream, char *buf, size_t size);
和setbuf类似,但是可以指定缓冲的大小
9.4.2 缓冲模式
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
// setvbuf(stdout, NULL, _IONBF, 0);// 将stdout设置为无缓冲
printf("Hello World!\n");
printf("Hello World!");
for ( ; ; )
sleep(1);
}
上面的代码只能看到第一个打印信息,第二个看不到,因为第一个是行缓冲,而第二个是全缓冲,只有当程序结束会刷新缓冲区。如果去掉注释,那么两个都可以打印出来。
9.4.3 刷新 stdio 缓冲区
#include <stdio.h>
int fflush(FILE *stream); // 刷新缓冲区
int fclose(FILE *stream); // 关闭缓冲区
强制刷新缓冲区,如果 stream 为 NULL,就表示刷新所有 stdio 缓冲区。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("Hello World!\n");
printf("Hello World!");
fflush(stdout);
// fclose(stdout);
for ( ; ; )
sleep(1);
}
上面代码一样可以看到两个打印信息。同样,关闭文件也可以刷新缓冲区
10. 文件描述符和FILE指针互转
有时需要将文件 IO 和标准 IO 混合使用
#include <stdio.h>
int fileno(FILE *stream);
FILE *fdopen(int fd, const char *mode);
当混合使用时,需要注意缓冲的问题,文件 IO 会直接将数据写入到内核缓冲区进行高速缓存,而标准 IO 会将数据写入到 stdio 缓冲区,之后再调用 write() 将 stdio 缓冲区中的数据写入到内核缓冲区。比如:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("print");
write(STDOUT_FILENO,"write\n",6);
exit(0);
}
// 这里先输出了write的内容,然后再输出print的内容,因为print没有遇到换行符,也就是清除缓冲区,只有等到运行结束之后才清理