C语言getchar和putchar是怎么实现的?
所有的stdio库函数,比如包括getchar/putchar/getc/putc之类的,都会经过stdio buffer:
如果从最简单的实现来说,可以把FILE结构体定义成下面这样 :
typedef struct _iobuf{
int fd; //文件描述符
int cnt; //缓冲区剩余字节数
char *base; //缓冲区地址
char *ptr; //缓冲区下一个字符地址
int flag; //访问模式
} FILE;
里面有5个参数,分别记录了:
- fd :系统调用open返回的文件描述符。
- base、ptr : 缓冲区地址、缓冲区下一个字符地址,上图标示了。
- cnt : 缓冲区剩余字符数,当cnt为0时,会系统调用read来填满缓冲区。
- flag : 文件访问模式,记录了文件打开方式、是否到达文件结尾等 。其中有一个字段,标识不进行缓冲,说明此种情况下每一次读取和输出都调用系统函数,可以使用setvbuf设置。一个例子就是标准错误流stderr : 当stderr连接的是终端设备时,写入一个字符就立即在终端设备显示。
enum _flags {
_READ = 1,
_WRITE = 2,
_UNBUF = 4, //不进行缓冲
_EOF = 8,
_ERR = 16
};
题主问的getchar可以这么实现:
int getc(FILE *f){
return --f->cnt >= 0 ? *f->ptr++ : fillbuf(f);
}
int getchar(){
return getc(stdin);
}
当缓冲区中还有剩余字符待读取时,即cnt>0,读取该字符并返回,并将缓冲区指针向后移动一个char单位,否则就调用fillbuf函数填满缓冲区,fillbuf的返回值就是待读取的字符。
fillbuf通过系统调用read读取一定字节的数据填满缓冲区:
int fillbuf(FILE *f){
int bufsize;
if((f->flag & (_READ | _EOF | _ERR)) != _READ){ //判断文件是否可读
return EOF;
}
bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;
if(f->base == NULL){ //没有分配过缓冲区
if((f->base = (char *)malloc(bufsize)) == NULL){
return EOF;
}
}
f->ptr = f->base;
int n = read(f->fd,f->ptr,BUFSIZ); //系统调用read
if(n == 0){ //到达文件结尾
f->base = NULL;
f->cnt = 0;
f-> flag |= _EOF;
return EOF;
}else if(n == -1){ //出错
f->cnt= 0;
f->flag |= _ERR;
return EOF;
}else{
f->cnt = --n;
return *f->ptr++;
}
}
- 判断文件是否可读。
- 判断调用read函数时应该读取的字节数,当文件设置了无缓冲时,读取1个字节,否则读取BUFSIZ个字节,BUFSIZ在<stdlib.h>中定义,定义了在该操作系统条件下缓冲区的最佳大小。
- 判断是否分配过缓冲区(fopen不会分配缓冲区,会再第一次调用getc时分配)。
- 调用系统函数read。
- 判断read返回值,分为到达文件结尾、出错和正常读取三种情况。
- 正常情况下返回缓冲区第一个字符给getc函数,并将cnt减1。
大概就是这样。
putchar也类似的实现:
int putc(int x,FILE *f){
return --f->cnt >= 0 ? *f->ptr++ = x : flushbuf(x,f);
}
int putchar(int x){
return putc(x, stdout);
}
将写入的字符放到ptr指向的位置,并将ptr向后移动一位。
当缓冲区满时,调用flushbuf将缓冲区内容刷新到文件中。实现一些flushbuf :
int flushbuf(int x,FILE *f){
if((f->flag & (_WRITE | _EOF | _ERR)) != _WRITE){ //判断文件是否可写
return EOF;
}
int n;
int bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;
if(f->base != NULL){
n = write(f->fd,f->base,f->ptr - f->base); //判断需要写入多少字节
if(n != f->ptr - f->base){
f->flag |= _ERR;
return EOF;
}
}else{
if((f->base = (char *)malloc(bufsize)) == NULL){
f->flag |= _ERR;
return EOF;
}
}
if(x != EOF){
f->cnt = bufsize - 1;
f->ptr = f->base;
*f->ptr++ = x;
}else{ //当写入EOF时,代表强制刷新缓冲区内容到文件中
f->cnt = bufsize;
f->ptr = f->base;
}
return x;
}
- 判断文件是否可写。
- 当已分配过缓冲区时,将缓冲区的内容通过系统调用write写入文件中。写入的字节数为
f->ptr - f->base
- 当没有分配过缓冲区时,分配缓冲区。
- 判断当写入的字符为EOF时,说明调用此函数的目的为强制刷新缓冲区,不写入字符。将cnt赋值为BUFSIZ,ptr赋值为缓冲区首地址base。
- 当写入字符不为EOF时,说明缓冲区已满,需要将缓冲区刷新到文件中。cnt为BUFSIZE - 1,将写入的字符x放到到缓冲区的第一格,然后将ptr向后移动一个char单位。
至于不通过getchar访问缓冲区,意思是想看buffer里的内容吗?
可以用setvbuf自己设置buffer。这段代码来自how does work setvbuf() in C [closed]
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int file_size;
char buffer[BUFSIZ];
FILE * fp = fopen("test.txt","w+");
if (setvbuf(fp,buffer,_IOFBF,BUFSIZ) != 0)
{
perror("setvbuf()");
fprintf(stderr,"setvbuf() failed in file %s at line # %d\n", __FILE__,__LINE__-3);
exit(EXIT_FAILURE);
}
/* Exhibit the contents of buffer. */
fputs ("aaa",fp);
printf("%s\n", buffer);
fputs ("bbb",fp);
printf("%s\n", buffer);
fputs ("ccc",fp);
printf("%s\n", buffer);
file_size = ftell(fp);
printf("file_size = %d\n", file_size);
fflush (fp); /* flush buffer */
printf("%s\n", buffer);
fputs ("ddd",fp);
printf("%s\n", buffer);
fputs ("eee",fp);
printf("%s\n", buffer);
rewind(fp); /* flush buffer and rewind file */
char buf[20];
fgets(buf,sizeof buf,fp);
printf("%s\n", buf);
fclose(fp);
return 0;
}
输出显示的就是buffer里的内容:
aaa
aaabbb
aaabbbccc
file_size = 9
aaabbbccc
dddbbbccc
dddeeeccc
aaabbbcccdddeee