当前位置: 首页 > C, C/C++ > 正文

探秘 stdio.h 设计与思考

前言

stdio.h   是我们经常使用的一个标准库。基本上现在的C编辑器都自动会在C文件中添加这个头文件。这一篇博客主要就是来了解这个库的前世今生。

——star

 stdio.h是什么?

这个头文件声明了很多的输入输出函数。当然几乎所有的用户级别程序都需要输入输出,事实上这也是C标准库出现的最早的头文件之一。并且它也是包含所有最多函数的头文件。所以相信探秘它也是需要很多的笔墨与精力。

最早的IO函数出现在上个世纪60年代,不过那个时候的输入输出程序的可移植性几乎没有,这一直也是那个时代计算机需要解决的问题,后来FORTRAN提出了一个思路就是编写更多的独立的输入输出函数。自此独立于设备的IO时代降临了。

后来70年代的早期UNIX诞生了,它将所有的文本流都采用了标准的内部形式,每一行以一个换行符来终止。

后来就有了ioctl,通过一种映射的机制控制文本处理设备。另一个修正文本流的方式是直接通过该设备的专门软件。对于每一个可能用到的设备来说,用户必须添加一个常驻系统的设备管理程序,这个东西让我想到了中断线相关的东西,就好比每一个设备都需要一个对应与自己的中断处理程序,这也是现代操作系统的一个设备管理方式吧。

再后来文件描述符诞生了,并且每一个运行的程序支持三个标准的文件描述符,标准输入,标准输出,标准错误。

但是我们每次打开一个文件都不可能选用函数IOCTL它需要指定一堆的参数,所以应当写一个标准库来封装这个函数。达到简单操作的目的。

文本行的长度:

有一些系统甚至不能表示空文本行,因此输出空文本行的时候,库实际上输出的可能就是包含一个空格的文本行,在读取的时候系统就会自动去掉只有一个空格的文本行。还有一些只能读取固定长度的文本行,但是明显这都是在早期的问题现在基本都已经解决了这个问题。

文件的长度:

某些系统不能表示空文件,如果创建了一个新文件但是没有写如任何都系,在关闭的时候,很有可能C标准会删除这个文件,因为在C标准看来一个空的文件没有什么用。

最终的结果:

如果使用UNIX的原语性能会收到影响,还有如果持续在用户空间创建缓冲区明显也不是一个明智之举。有些IO程序就开始不使用UNIX原语,但是站在标准的角度来看我们可能为了解决一个问题而产生多个处理程序,所以最终还是规定使用流,添加FILE类型来处理问题。也就是我们现在使用的fopen 函数族。

C标准的内容:

<STDIO.H>总共定义了三种类型,一些宏和很多的执行输入数出的函数。

声明的类型:

size_t    ,  FILE   ,fpos_t   简要说一下最后一个类型:这是一个对象类型,可以指定唯一的文件的每一个位置所需要的信息。

声明的宏:

_IOFBF

_IOLBF

_IONBF

这个三个宏是setvbuf 的三个参数使用。

BUFSIZ

这个宏是指setbuf 函数所指的缓冲区大小

EOF

文件爱你描述符,很常见。

FOPEN_MAX

一个应用中可以同时打开的所有文件描述符数量。

FILENAME_MAX

一个文件名的最长大小,注意这里是使用char 数组存储的函数名。

L_tmpnam,TMP_MAX。

表示使用tmpnam.最大生成文件名,以及长度。

SEEK_CUR,SEEK_END,SEEK_SET 不多说很熟悉fseek函数的三个参数,用来设置文件指针的位置状态。

stderr,stdin,stdout  指向 FILE的类型指针。

环境限制:

实现支持的文本文件的每一行应该至少可以包含254个字符,包括结束的换行符。宏的BUFSIZE的值至少应该为256.

每个程序开始后其实已经打开了三个标准流,所以宏 FOPEN_MAX至少是3.

定义的函数:(这里直说很少用到的函数,常用的包括LINUXC不再说明范围内,因为已经很熟了)

remove.

rename.

tmpfile.

FILE *tmpfile(void)

这个函数创建一个临时二进制文件,当这个文件关闭程序或者终止的时候,它会被自动的删除。如果程序异常终止,打开的文件是否会删除这是根据实现决定的,这个文件打开时通过模式“wb+”进行更新。

返回值是创建的文件的流指针,如果不能创建,函数返回一个空指针。

 

char *tmpnam(char *s)

这个函数生成一个字符,这个字符是一个有效的文件名,并且不和现有存在的文件相同。这个函数每次调用一次都会生成一个不同的名字最多可以调用TMP_MAX次,如果超过这个次数那么行为将是自定义的。

实现应当假设库函数没有调用函数tmpnam.

返回值:

如果参数是一个空指针,函数就把它的结果保留在一个内部的静态对象中,并返回指向该对象的指针,对这个函数的后续调用可能会修改该对象,如果参数不是一个空指针,那么它指向一个至少有L_TMPNAM个字符元素的数组,函数tmpnam把结果写到该数组中并返回参数值。

环境限制:

宏TMP_MAX的值至少是25.

 

文件访问函数:

这里依然只是说明下我们不经常用的,经常用的函数仅仅只是列举出来并不进行说明。

fclose( ) 函数,关闭文件描述符。

fopen( )函数,打开文件,返回一个文件描述符。其中的参数可以自行百度很简单。

int fflush(FILE *stream);

如果这个流指向一个输出流或者修改流,在这个流中,最近的操作不是输入,函数fflush 会把这个流未写入的数据传递给宿主环境,然后写入文件,个人感觉就是变向清空了流缓冲区,但是执行结果是直接写入文件而不是丢弃。

如果参数是是空指针,这个函数对所有的流执行上述定义的清空行为。也算是清空缓冲区的一种情况吧。

 

FILE *freopen(const char *filename , const char *mode, FILE *stream)

这个函数打开名为filename 的函数并且把它和stream 指向的流关联在一起,其中权限和fopen 中权限相同,这个函数首先会尝试关闭和指定的流关联的任意文件。如果关闭失败清空错误提示符和文件结束符。

失败返回空指针,否则返回stream 的值。

int setvbuf(FILE * stream,char *buf,int mode,size_t size);

这个函数只能在stream 指向的流和一个文件向关联的时候使用,并且对流执行任何其他操作之前。参数mode 决定了stream 缓冲的方式。具体如下:

_IOFBF  导致输入输出完全缓冲

_IOLBF  导致输入输出行缓冲

_IONBF  导致输入输出不缓冲

如果buf 不是一个空指针,可以使用它指向的数组来代替setvbuf 函数分配的缓冲区。size  指定了数组的大小,数组的内容在任何时候都是不确定的。

返回值:

函数成功是返回0,其他时候返回非0;

 

格式化输入输出函数:

fprintf(FILE *stream, const char *format,….);

fscanf(FILE *stream,const char * format,….);

sprintf(char *s,const char *format,….);

sscanf(const char *s,const char *format,…);

vfprintf(FILE *stream,const char *formatm,va_list arg);

vprintf(const char *format,va_list arg);

这些函数参数比较复杂,但是差不多都是使用了变参函数原理实现了格式化输出或输入。难度并不大。

字符输入输出函数:

int fgetc(FILE *stream);

这个函数其实使用起来比较简单,但是博主还是想看看这个函数。

这个函数从给定的输入流中指向的下一个字符,如果输入流在文件结束处,则文件设置文件结束符,函数返回EOF。如果函数错误就设置流的错误提示符并且返回EOF。

int fgets(char *s,int n,FILE *stream);

这个函数其实也值得深入了解下,因为C语言的一个特性就是不检查边界,但是我们为了程序的安全使用属所以必须检查,想上个世纪的“蠕虫”病毒就是因为缓冲区溢出造成的。

函数冲stream 指向的流读取字符,读取的数目比n 指定的字符数目少一个,然后将字符写入s指向的数组中。输入换行符或者文件结束后,不再读取其他的字符,最后一个字符写入数组后立即写入一个空字符。

返回值:

如果成功,就返回S,如果与到文件描述符结束并且没有向数组中写入字符,则数组内容不变并且返回一个空指针。如果操作过程中发生了读取错误,则数组的内容不确定,并且返回一个空指针。

fputs(const char *s,FILE *stream)

函数把s指向的串写入了stream指向的流中,不写入结束的空字符。

int getc(FILE *stream)

函数getc 返回stream 指向的输入流的下一个字符,如果流处于文件结束处,则设置流的文件结束指示符。并且这可能是一个宏实现。

int  getchar(void);

函数getchar 等价与用参数stdin 调用的函数getc.

int ungetc(int c,FILE *stream);

这个函数把C指定的字符,首先转换为unsigned char 类型,退回到stream 指向的输入流中,回退字符是通过对流的后续读取并且按照反顺序来返回的。如果中间成功调用了一个文件定位函数,那么就会丢弃流的所有退回的字符。流的相应外部存储保持不变。

回退的一个字符是受保护的,如果对同一个流调用ungetc函数太多次,中间有没有读写,缓冲区饱和,这种操作就会失败。

如果C的值和宏EOF的值相等则操作失败且输入流保持不变。

成功调用过后会清空流的文件结束符。

 

int fgetpos(FILE * stream,fpos_t *pos) ;

函数会把stream指向的流文件定位到当前值存储到pos 指向的对象中,存储的值包含了未指明的信息。函数fsetpos 函数可以利用这些信息将流重新定位到调用此函数的地方。

long int ftell(FILE * stream);

这个函数获得流指向的文件定位符的当前值。对于一个二进制流来说,这个值是从文件开头到当前位置的字符数目。对于一个文本流来说,它的文件定位符包含了未说明的信息。

void  rewind(FILE *stream);

函数把流指定的流文件定位符设置在文件的开始位置它等价与

(void )fseek(stream,0L,SEEK_SET),只不过流的错误提示符也被清0.

错误处理函数:

void  clearerr(FILE * stream)

函数清空流指向的流文件的文件结束符和错误提示符。没有返回值。

int  feof(FILE *stream)

函数测试stream指向的流的文件结束符。如果设置了文件结束符则返回一个非0值。

 

int  ferror(FILE *stream)

函数测试stream 流设置了错误提示符时函数时返回非0;

int perror(const char *s);

函数把整数表达式中的错误编号转换成一条错误信息。

<stdio.h>的实现

首先对于这种IO类型的库来说,有两个方面的设计至关重要。

一个是数据结构FILE的设计

另一个是与操作系统相互作用以指向输入输出的低级原语。

首先是一些需要的参数:

#define   _NULL           (void *)0     /*NULL*/

#define   _FNAMAX    64                /*FILENAME_MAX*/

#define   _FOPMAX    32                /*FOPEN_MAX*/

#define   _TNAMAX    16                /*L_tmpnam*/

现在我们看看FILE的类型这个类型我们经常使用却没有探究过其中的具体内容,今天我们探究一下这个类型的具体结构。

_Mode 流的状态位集合

_Handle  操作系统为打开文件返回的句柄或文件描述符

_Buf       指向缓冲区的首地址指针,如果没有分配缓冲区,则是一个空指针

_Bend    指向超出缓冲区的第一个字符的指针,如果为空则没有意义。

_NEXT  指向读操作或写操作的下一个字符的指针,它不可能是一个空指针。

_Rend   指向超出读取范围的第一个字符指针,它不可能是空指针。

_Rsave   如果字符回退,则保存指针_Rend

_Wend   指向超出可写数据范围的第一个字符,它不可能是一个空指针。

_Back    保存回退字符的栈

_Cbuf     当没有其他缓冲区可用的时候能够使用单字符缓冲区

_Nback   记录回退字符个数

_Tmpnam 指向文件关闭时要删除的临时文件名的指针,或一个空指针。

未完待续V0.2

 

本文固定链接: http://zmrlinux.com/2015/11/24/%e6%8e%a2%e7%a7%98-stdio-h/ | Kernel & Me

该日志由 root 于2015年11月24日发表在 C, C/C++ 分类下, 你可以发表评论,并在保留原文地址及作者的情况下引用到你的网站或博客。
原创文章转载请注明: 探秘 stdio.h 设计与思考 | Kernel & Me
【上一篇】
【下一篇】