C 语言与缓冲区

有关缓冲区的概念和 C 语言中相关的内容...

在了解二者联系之前,先要明白缓冲区的概念。

缓冲区

所谓缓冲区(Buffer),就是指暂时存放输入或输出内容的一个内存空间,并且这个空间的大小是一定的。
那为什么要设置缓冲区呢?
目的是为了将高速读写设备与低速读写设备同步起来。举个简单的例子,打印机的打印速度比较慢,而电脑操作指令的响应速度比较块,但在打印机打印的同时,可以操作电脑不断把要打印的内容发送到打印机(电脑操作不用等待),对应打印机就会依次执行打印任务。

另外,需要注意区分缓冲区(Buffer)和缓存(Cache)的概念,这里不做深究。

C

在 C 语言中也有用到缓冲区这个“设计模式”,之所以称之为设计模式,一是笔者不确定其他语言是否也是如此(只能确定大部分语言是如此);二是就笔者的个人理解,将缓冲区称呼为一种(计算机领域的)设计模式更恰当,因为不止程序语言的设计如此,部分硬件的设计也是有这个模式的存在。
不过,这里不深究其他的内容,只专注 C 语言中与缓冲区相关的内容。

C 语言中存在三种缓冲模式:全缓冲(fully buffered)、行缓冲(line buffered)和无缓冲(unbuffered)。
在探讨各种缓冲模式之前,先得明确一下 C 语言中流的概念,可以参考一下百度百科——流。实际上,可以把 C 语言中通过fopen函数打开文件得到的文件指针看作流的入口,当通过这个文件指针向文件读写数据时,这些数据就是流,这个过程就是“流动”。
对应的,C 语言中提供了三个标准流:标准输入流(stdin)、标准输出流(stdout)和标准错误流(stderr),其中stdinstdout一般是行缓冲,stderr一般是无缓冲的(为的是能第一时间输出错误信息)。
写到这里,会有一个疑问,使用fopen函数打开的文件流,默认的缓冲模式是什么呢?一般是行缓冲,可能是全缓冲,这取决于编译器具体的实现,但可以通过setvbuf函数来修改流的缓冲模式。

好了,其他的问题暂时不谈了,下面来探究一下各种缓冲模式。
PS:以下测试均在 Ubuntu 16.04 环境下进行。

全缓冲

全缓冲就是在缓冲区填满了之后,才执行 I/O 操作。测试代码:

test1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>

#define FILEPATH "./tmp"
const char *string = "ABCDEFG";

int main() {
char buff[1024];

FILE *fp = NULL;
fp = fopen(FILEPATH, "w+");
if(fp == NULL) {
perror("fopen() error!\n");
}
int ret = 0;
ret = setvbuf(fp, buff, _IOFBF, 1024);
if(ret != 0) {
perror("setvbuf error!\n");
}
fprintf(fp, "%s", string);
while(1){

}
return 0;
}

在上面的代码中,首先使用fopen函数在w+(读,若文件不存在则创建)的模式下打开文件,接着使用setvbuf函数设置缓冲模式为_IOFBF(全缓冲)。运行程序,在另一个终端中可以确认tmp文件已创建,然后键入

1
$ cat tmp

显示文件无内容。

行缓冲

行缓冲就是在遇到换行符(\n)时,执行 I/O 操作,典型代表就是标准输入的scanf函数,每次输入完数据和回车后,才能看到效果。测试代码:

test2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>

#define FILEPATH "./tmp"
const char *string = "ABCDEFG\n";

int main() {
char buff[1024];
FILE *fp = NULL;
fp = fopen(FILEPATH, "w+");
if(fp == NULL) {
perror("fopen() error!\n");
}
int ret = 0;
ret = setvbuf(fp, buff, _IOLBF, 1024);
if(ret != 0) {
perror("setvbuf error!\n");
}
fprintf(fp, "%s", string);
while(1){

}
return 0;
}

相比 test1,上面的代码有两个改动:

  1. const char *string = "ABCDEFG\n";,加入\n后,程序才会进行 I/O 操作
  2. ret = setvbuf(fp, buff, _IOLBF, 1024);,修改为行缓冲模式

同样的思路,运行程序,在另一个终端中可以确认tmp文件已创建,然后键入

1
$ cat tmp

显示:

1
ABCDEFG

无缓冲

无缓冲就是不进行缓冲,直接执行 I/O 操作。测试代码:

test3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>

#define FILEPATH "./tmp"
const char *string = "ABCDEFG";

int main() {
char buff[1024];
FILE *fp = NULL;
fp = fopen(FILEPATH, "w+");
if(fp == NULL) {
perror("fopen() error!\n");
}
int ret = 0;
ret = setvbuf(fp, buff, _IONBF, 1024);
if(ret != 0) {
perror("setvbuf error!\n");
}
fprintf(fp, "%s", string);
while(1){

}
return 0;
}

相比 test2,上面的代码有两个改动:

  1. const char *string = "ABCDEFG;,与 test1 保持一致
  2. ret = setvbuf(fp, buff, _IONBF, 1024);,修改为无缓冲模式

同样的思路,运行程序,在另一个终端中可以确认tmp文件已创建,然后键入

1
$ cat tmp

显示:

1
ABCDEFG(account)$xxx

上面的(account)$xxx是终端显示的用户名,因为没有\n,所以会与终端的用户名显示在一行。

如何刷新缓冲区

明确各种缓冲模式的特点后,也要明白如何刷新缓冲区。
其实上面已经提到一种刷新缓冲区的方法了,在行缓冲模式下,只需要在向流输入一个\n,就会刷新缓冲区。
而 C 标准库也提供了fflush函数用来刷新缓冲区。
使用 test1 中的代码:

test4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>

#define FILEPATH "./tmp"
const char *string = "ABCDEFG";

int main() {
char buff[1024];

FILE *fp = NULL;
fp = fopen(FILEPATH, "w+");
if(fp == NULL) {
perror("fopen() error!\n");
}
int ret = 0;
ret = setvbuf(fp, buff, _IOFBF, 1024);
if(ret != 0) {
perror("setvbuf error!\n");
}
fprintf(fp, "%s", string);
fflush(fp);
while(1){

}
return 0;
}

相比 test1,上面的代码有个改动:

  1. fflush(fp);,手动刷新缓冲区

同样的思路,运行程序,在另一个终端中可以确认tmp文件已创建,然后键入

1
$ cat tmp

显示:

1
ABCDEFG(account)$xxx

显示结果与 test3 一致。

调用fflush函数后就会刷新缓冲区,任何一种缓冲模式下,都可以使用该函数来刷新缓冲区。

结语

C 语言中有关缓冲区的知识暂时就记录到这里了,如前所说,“缓冲”更多是一种思想,从这个角度去理解,应该能收获更多东西。


Buy me a coffee ? :)
0%