平常在练习的时候,总是忽略IO的缓存,这篇笔记记录下C和C++的缓存问题。
1 什么是缓存
缓存就是程序在内存中开辟的用来存放数据的空间,之所以叫缓存是因为这个变量时用来暂存数据用的。比如下面的语句,
int a ;
int *p = malloc(sizeof(int) * 1);
a 和 p都是缓存,
2 应用缓存、库缓存、内核缓存,驱动缓存
应用缓存:应用代码在内存中开辟的缓存空间,比如应用层定义的变量。
库缓冲:库函数代码在内存中开辟的缓存空间。
内核缓存:内核代码在内存中开辟的缓存空间。
驱动缓存:驱动层的层序开辟的缓存,工作中驱动层接收到ROS层传下的数据进行存储并处理。
3 标准IO读写数据时,数据在缓存中流向
应用程序读写文件时候,数据并不会直接写到文件中,而是一层层向上或向下流动。
比如读数据时,内核函数通过设备驱动代码将数据从文件中读到内核缓存中;
标准IO函数,比如从外部设备读数据时用的cin scanf等IO函数,数据的执行流程是,将数据从内核缓存读到IO库缓存中。
最后,应用程序从IO库缓存中读取数据。
写数据的过程与读数据正好相反。
4 库缓存的三种缓冲方式:无缓冲,行缓冲,全缓冲
缓冲就是暂存的意思,就是把数据暂时存在库缓存中,库缓存一般为4096字节。
4.1 为什么需要暂存?
之所以要暂存是处于速度方面的考虑。比如,只输入一个字符,就立刻调用文件IO将数据写到磁盘,会降低效率,所以先将数据暂存起来,当达到“刷新”条件时候(刷新:就是将缓存中的数据取走),再一次性的将缓存的数据写到文件中。
4.2 无缓冲
无缓冲就是,主要库缓存中有数据就立刻刷新,哪怕只有一个字符也会立刻刷新。无缓冲非常适合用于输出错误信息,因为出错信息很紧急,不能被积压,所以有了错误信息后就应该被立即无条件刷新输出。stderr(标准出错输出)就是专门用来输出错误信息的,所以就是“无缓冲”的情况。无缓冲特点是数据立刻被刷新,但是效率会比较低。
无缓冲的刷新方式
只要“库缓存”中有数据就无条件立即刷新输出,比如下面的例子。
#include <stdio.h>
void main()
{
fprintf(stderr, "hello wolrd");
while(1);
return 0;
}
hello wolrd被立即输出。
4.3 行缓冲
数据会积压,直到“满一行”时才会刷新,然后将数据输入/输出。\n就是一行的判断条件之一,只要数据中有\n,就代表数据满一行了。也就是说,对于标准IO库的库缓存来说,\n不仅仅只是换行,还是数据满一行的标志。
行缓冲非常适合用于正常的键盘输入和打印显示(正常的人机交互),人与计算机的交互就跟人与人的说话一样,一句一句的来是最方便的。人机交互时一行其实就是一句,**stdin、stdout(标准输入、标准输出)**就是专门用来进行正常人机交互用的,所以都是行缓冲的,满足一行的条件时就会立即刷输入、输出数据。C标准中,stderr为无缓冲,stdin和stdout为行缓冲。
但是不同的OS中不一定会严格的按照C标准,比如:
在Linux下:stderr为“无缓冲”、stdin、stdout为“行缓冲”,遵守了c标准的规定;
在Windows下:stderr、stdin、stdout都是无缓冲,只要库缓存有数据就会立即刷新。
行缓冲刷新方式
#include <stdio.h>
void main()
{
fprintf(stdout, "hello wolrd");
fprintf(stdout, "@@@@@@@@@@@");
printf("22222222222");
fprintf(stdout, "!!!!!!!!");
while(1); //死循环
return 0;
}
【linux下运行结果】
【windows下运行结果】
上面实验代码中,LInux下运行时,如果加了\n时,即fprintf(stdout, “!!!!!!!!\n”);数据就会全部被输出。
4.3 全缓冲
特点是必须等到“库缓存”的空间全部被数据积压满后,才会输出。就好比说以一盆水满了以后才会溢出一样。读写硬盘上普通文件时就是“全缓冲”,比如以写为例,写普通文件时由于不涉及人机交互时的“及时性”问题,所以并不需要立即写到文件中,完全可以先将大量数据积压到缓存中,等填满整个缓存之后再一次性刷新输出。读写fopen所打开的普通文件时,就是“全缓冲”的。
全缓冲的刷新方式
fp指向了普通文件,此时就是全缓存的,尽管"hello world\n"中有个一\n,但是全缓冲不受\n影响,由于数据太少,还不足以填满整个库缓存,所以无法被刷新,此时打开file.txt文件时,里面不会有数据。当调用fclose()函数时候,数据就会被刷新。
5 缓冲的其它刷新方式
当不满足刷新条件时,我们可以使用其它“刷新方式”来刷新?可以调用fflush函数进行刷新。
#include <stdio.h>
int fflush(FILE *stream);
下面例子
void main(void)
{
fprintf(stdout, "hello wolrd");
fprintf(stdout, "@@@@@@@@@@@");
printf("22222222222");
fprintf(stdout, "!!!!!!!!");
fflush(stdout); //手动刷新
while(1); //死循环
return 0;
}
6 平时练习时,cin,scanf读取数据时存在的问题
从键盘输入数据时,不管输入什么数据,只有敲了回车(\n)后才会输入,从键盘输入数据时,输入的最后输入的字符一定是’\n’。
但是scanf等读取函数从“库缓存”里面读取数据时,往往会将\n留在“库缓存”里面,如果后续紧跟着scanf/fscanf/getc/getchar等函数来读取数时,读取到的将是“库缓存”中遗留的’\n’,或者其它无用字符。
比如,输入w \n ,可以看到第二个scanf读取到了回车。
**注意:**以%d、%f格式输入时,输入的必须是0~9的数字,如果是a、b、c、d的英文字母的话,会直接将这些字符遗留在库缓存中,影响后续的读取。
如何解决呢?
很简单,只要将“库缓存”中所有的字符全部取走,将“库缓存”全部清空后,就不会受到这些遗留字符的干扰,不管使用getchar、getc、scanf(“%c”, &ch)中那个函数,都能将遗留的字符全部取走,下面使用最简洁getchar来实现。
比如以%d接收数字时候,输入的a后,就不会被解析,而是留在缓冲中。
这种情况下就可以用scanf将缓冲中的数据全部读走,学习时候最常用的就是while(getchar() != ‘\n’); 方式。
上面都是学习过程中遇到的问题,实际工作中,最多的是通过条件编译输出,并没有遇到输入的时候。