标准IO的引入
缓存大小和执行时间的关系
最优缓存大小:
分别为用户CPU时间和内核CPU时间
引入标准IO库的目的是为了提高IO的效率,避免频繁的进行read/write系统调用,而系统调用会消耗较多的资源。标准I/O在系统调用的上一层多加了一个缓冲区,通过缓冲机制减少了系统调用,实现更高的效率。
一个关于缓存的实验现象:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello world");
for (int i = 0; i < 10; ++i) {
sleep(2);
}
}
点击执行,屏幕上并没有先显示出“hello world” 而是在20s之后或者程序退出之后才显示。
当我们修改一下代码,在hello world 后面加上一个“\n”时:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello world\n");
for (int i = 0; i < 10; ++i) {
sleep(2);
}
}
可以看到,此时是立刻先输出了hello world。
为什么产生这种现象,原因是什么呢?
这是由于缓存的原因,printf不是直接系统调用函数,它在直接写的时候并没有写给系统调用write()函数(底层函数只有wirte才能写),而是写入一个缓存区,当缓存区达到一定条件后才会触发系统调用write()函数,才会在屏幕上显示。
标准IO是在不同系统调用之前加入一个缓存,让所有操作不是直接访问,而是现在缓存中存一存,等确实有必要的时候才进行系统调用,这样避免了频繁进行系统调用,提高了IO效率
标准IO的特点
- 不仅在UNIX系统,在很多操作系统上都实现了标准IO库
- 标准IO库由ANSIC标准说明
- 标准IO库处理很多细节,如缓存分配、以优化长度执行IO等,这样用户不必关心如何选择合适的块长度
- 标准IO在系统调用函数基础上构造的,它便于用户使用
- 标准IO库及其头文件stdio.h为底层IO系统调用提供了一个用户接口
文件指针
- FILE指针:每个被使用的文件都在内存中开辟一个区域,用来存放文件的有关信息,这些信息是保存在一个结构体类型的变量中,该结构体类型是由系统定义的,取名为FILE。
- 标准IO库的所有操作都是围绕流(stream)来进行的。在标准IO中,流用FILE*来描述
- 标准IO库是由Dennis Ritchie在1975年左右编写的
缓存文件系统(高级磁盘IO)
- 目的:尽量减少使用read/write的调用
- 定义:系统自动的在内存中为每一个正在使用的文件开辟一个缓冲区,从内存向磁盘输出数据必须先送到内存缓冲区,装满缓冲区在一起送到磁盘中区。从磁盘读取数据,则一次从磁盘文件将一批数据读入到内存缓冲区,然后再从缓冲区逐个的将数据送到程序的数据区
分类:全缓存、行缓存、不缓存
全缓存:缓存区满了
行缓存:“/n”
不缓存:绕过缓冲区直接操作
非缓冲文件系统(低级磁盘IO)
定义:依靠于操作系统,通过对操作系统的功能对文件进行读写,是系统级的输入输出。
标准IO提供的三种缓存类型
- 全缓存(针对所有文件)
- 当填满IO缓存后才进行实际IO操作,或者满足一定条件后,系统通过调用malloc()来获得所需要的缓冲区域,默认值。
- 当缓冲区满了,或者满足一定的条件后,就会执行刷新操作
- 行缓存
- 当在输入和输出中遇到新行符“\n”时,进行IO操作
- 当流遇到一个终端时,典型的行缓存(如敲键盘之类:敲完之后回车)
- 不带缓存
- 标准IO库不对字符进行缓冲,例如stderr
- 很多的人机交互界面要求不可全缓存(如手机屏幕,点击实时反应)
- 标准出错绝不会是全缓存的
- 在任何时刻,可以使用fflush强刷新一个数据流
printf、fprintf、sprintf
printf(const char *format,…) 函数用于格式化输出数据,将输出结果到标准输出设备,直到出现字符串结束“\0”为止。
fprintf(FILE * stream, const char * format,… ) 函数会根据参数format字符串并转换格式化数据,然后将结果输出到参数stream指定的文件中,直到出现字符串结束(\0)为止。
sprintf(char *str, const char * format, …) 函数会根据format字符串来转换并格式化数据,然后将结果复制到参数str所指的字符串数组,直到出现“\0”为止。
案例
- 编写程序向标准输出输出 “stdout: hello world”
- 编写程序向错误输出输出 “stderr: 123”
- 控制标准的标准输出输出,使程序仅输出标准输出字符
- 控制标准的输出错误输出,使程序仅输出错误输出字符
#include <stdio.h>
#include <unistd.h>
int main()
{
fprintf(stdout,"hello world");
fprintf(stderr,"123");
for (int i = 0; i <5; ++i) {
sleep(1);
}
}
实验现象:
实验现象:先出123,在程序退出时才显示hello world,而代码中是先写的输出hello world,再输出123。
原因:因为fprintf(stdout,“hello world”);是行缓存,如果在末尾没有加’\n"就不会立刻输出
这里的fprintf(stdout,“hello world”);等价于平时使用的printf()
输出重定向:
在liunx命令行中: > 表示标准输出重定向
2> 表示输出错误重定向
执行以下代码:
#include <stdio.h>
#include <unistd.h>
int main()
{
fprintf(stdout,"hello world\n");
fprintf(stderr,"123");
}
执行命令:./ClionProject > a.txt
可以看到,执行>命令后屏幕上没有输出结果了,输出结果被重定向到a.txt 中了
但是a.txt中只有hello world,没有123
再次执行命令:./ClionProject 2> a.txt
屏幕上出现了hello world
打开a.txt:
123被重定向了
如果想把标准输出和标准错误同时重定向:“&>”
再次执行命令:./ClionProject &> a.txt
总结:“>” 为标准输出重定向,"2>“为标准错误重定向,”&>"同时标准输出和错误重定向
用处:在工作中,编写程序时,打印重要信息单独摘抄出来。
用于打开一个标准IO流的三个函数:
FILE *foen(const char *path, const char *mode);
FILE *freopen(const char *parthname, const char *type, FILE *fp);
FILE *fdopen(int filedes, const char *type);
注:fopen需要和fclose成对出现,因为fopen返回的地址空间是堆,而堆空间是必须让程序员手动释放
实验——缓存文件的创建
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#define NEW_FILE_NAME "/tmp/temp/tmp_new.txt"
/*
* 标准IO将文件描述符、缓存等数据结构抽象成FILE,使用一个FILE *指向这些对象
* 标准IO使用fopen实现这些结构的申请,申请内容在堆上,所以在不使用这些结构时,一定要释放,使用fclose函数
*
* 文件不存在,创建新文件 : "w",这个选项会清空原内容
* 不改变文件原内容的话,使用"r"方式
* "+"符号指扩展标记,w就扩展r,r就扩展w,相当于一定是读写方式
* 如果文件不存在就创建,存在就保留原文件,使用"a"标签,a只能向后添加
* */
int lesson1(const char *filename) {
FILE *fp;
// 如果目录不存在就创建
mkdir("/tmp/temp/",0777);
fp = fopen(filename, "w");
if (fp == NULL) {
perror("fopen:");
return -1;
}
for(int i =0;i<12;i++){
int month = i+1;
int year = 2023;
fprintf(fp,"%d-%02d\n",year,month);
}
fclose(fp);
}
int main() {
lesson1(NEW_FILE_NAME);
}
实验结果:
实际工程开发中,不允许使用的函数:strcpy、gets(容易越界、内存泄露)
fgets(buf, sizeof(buf), stdin);
用strncpy、fgets(作为输入,不用scanf)