与系统I/O函数不一样的是,标准I/O通常提供了缓冲功能,将数据先缓冲在内存中,当缓冲区满后再自动进行读或写操作,或者强制flush缓冲区进行读写操作,这样可以避免频繁的系统调用,使系统频繁切换于内核态和用户态之间,提高I/O效率。
缓冲区的理解:
用户程序调用C标准I/O库函数读写文件或设备,而这些库函数要通过系统调用把读写请求传给内核,最终由内核驱动磁盘或设备完成I/O操作。C标准库为每个打开的文件分配一个I/O缓冲区以加速读写操作,通过文件的FILE
结构体可以找到这个缓冲区,用户调用读写函数大多数时候都在I/O缓冲区中读写,只有少数时候需要把读写请求传给内核。以fgetc
/fputc
为例,当用户程序第一次调用fgetc
读一个字节时,fgetc
函数可能通过系统调用进入内核读1K字节到I/O缓冲区中,然后返回I/O缓冲区中的第一个字节给用户,把读写位置指向I/O缓冲区中的第二个字符,以后用户再调fgetc
,就直接从I/O缓冲区中读取,而不需要进内核了,当用户把这1K字节都读完之后,再次调用fgetc
时,fgetc
函 数会再次进入内核读1K字节到I/O缓冲区中。在这个场景中用户程序、C标准库和内核之间的关系就像CPU、Cache和内存之间的关系一样,C标准库之 所以会从内核预读一些数据放在I/O缓冲区中,是希望用户程序随后要用到这些数据,C标准库的I/O缓冲区也在用户空间,直接从用户空间读取数据比进内核读数据要快得多。另一方面,用户程序调用fputc
通常只是写到I/O缓冲区中,这样fputc
函数可以很快地返回,如果I/O缓冲区写满了,fputc
就通过系统调用把I/O缓冲区中的数据传给内核,内核最终把数据写回磁盘。
I/O缓冲分三种:
全缓冲:填满缓冲区后才进行实际I/O操作,在一个流上第一次执行I/O操作时,相关标准I/O函数通常调用malloc获得需使用的缓冲区。
行缓冲:在输入或输出中遇到换行符时,执行I/O操作。限制:行缓冲区大小时固定的,当一行内容超过这一限制时,也会进行I/O操作。
无缓冲:不对字符进行缓冲存储。
强制冲洗一个流的函数:int fflush( FILE *fp), 成功返回0,出错返回EOF
流打开:
fdopen(),是打开指定文件描述符,并将标准I/O流与之相结合。
关闭流:
int fclose(FILE *fp); 在关闭流前,会将输出缓冲区中的输出数据,而缓冲区中的输入数据会被丢弃。并释放缓冲区所占的内存。
读写一个字符
函数getchar等价于 getc(stdin)。返回的是ASCII字符对应的整型值。getc是宏实现,fgetc是函数调用。
每次只能回送一个字符,回送的字符可以自己指定,不可回送EOF。回送abc,读出顺序为cba。常用于我们需要看下一个字符是什么而决定本次操作的情况。
对应的输出一个字符的函数:
putchar默认输出到屏幕上,而putc,fputc是输出到指定的文件流上。
读写一行字符:
同样,puts直接输出到屏幕。
记:以c结尾的都是读一个字符,且返回的是一个整型数字。以s结尾的是读取一行。getschar, putchar, gets, puts默认的输入输出流都是键盘或屏幕。
疑问:标准I/O在读入一个字符时是读入一个字节还是两个字节的?读入中文时是怎么处理的?
疑问来源:hehe是文本内容:hello world!\n中文
代码1 io_one_char.c :读一个字符,先printf输出,再putchar输出
代码2 io_one_char2.c:直接读一个,putchar
/io_one_char.c
#include<stdio.h>
int main(){
FILE *f;
f=fopen("hehe","r");
int c;
while((c=fgetc(f))!=EOF){
printf("%d:",c);
putchar(c);
}
return 0;
}
//io_one_char2.c
#include<stdio.h>
int main(){
FILE *f;
f=fopen("hehe","r");
int c;
while((c=fgetc(f))!=EOF){
//printf("%d:",c);
putchar(c);
}
return 0;
}
结果:第一个出现流乱码,不能正常显示中文了。还没想通为什么,先记录下。
二进制I/O
计算机并不区分二进制文件与文本文件,所有的文件都是以二进制形式来存储的,因此本质上讲,所有文件都是二进制文件。在文本I/O中自动进行编码和解码:在写入一个字符时,程序会将统一码转换成文件指定的编码,而在读取字符的时候,将文件指定的编码转化成统一码。
而对于二进制文件,没有编码格式,这种情况下我们更愿意一次读或写整个结构。因此,提供了下列两个函数以执行二进制I/O操
使用二进制I/O的基本问题是,它只能用于读在同一系统上已写的数据。
(1)在一个结构中,同一成员的偏移量可能因编译器和系统而异(由于不同的对准要求)。确实,某些编译器有一个选项,选择它的不同值,或者使结构中的各成员紧密包装(这可以节省存储空间,而运行性能则可能有所下降);或者准确对齐(以便在运行时易于访问结构中的各成员)。这意味着即使在同一个系统上,一个结构的二进制存放方式也可能因编译器选项的不同而不同。
(2)用来存储多字节整数和浮点值的二进制格式在不同的机器体系结构间也可能不同。
文本文件在读写时,用来判断是否到达文件末尾通常是与EOF比较,一般EOF为int型数字-1,因为文本文件都是字符,在0-255之间,不存在-1,所以可以用EOF判断是否到达流尾部。但在二进制文件中,通常是用块的方式来读取,不与EOF比较。Linux下,按下Ctrl-D,就代表EOF,windows下按Ctrl-Z表示EOF。
格式化I/O
sprintf可能因缓冲区不够大而造成内存泄漏,snprintf函数会对超过缓冲区部分的字符流截断丢弃,不写入内存。
临时文件
tmpfile产生一个临时二进制文件,在关闭该文件或程序退出时会删除掉该临时文件。
#include<stdio.h>
int main(){
char tmpname[L_tmpnam];
printf("%s\n",tmpnam(NULL)); //产生一个文件名
tmpnam(tmpname); //产生第二个文件名
puts(tmpname);
//创建文件
FILE *fl;
char line[50];
fl=tmpfile();
fputs("hello world!\n",fl);//向该文件写入内容
rewind(fl); //重置偏移量为0
fgets(line,sizeof(line),fl); //读
fputs(line,stdout); //输出到屏幕
return 0;
}
这种情况下通常是先产生一个随机路径名,再基于这个路径创建文件。然后还提供流另外两个高级一点的创建临时目录和临时文件的函数。
mkdtemp创建一个唯一临时目录,mkstemp创建唯一临时文件,且程序退出后不删除。
#include<stdio.h>
#include<stdlib.h>
int main(){
char good_name[]="/tmp/dirXXXXXX"; //char *good_name="tmp/dirXXXXXX";
if(mkstemp(good_name)==-1)
printf("wrong");
else
printf("%s\n",good_name);
return 0;
}
运行结果,发现tmp下多了一个临时文件,该文件仅创建者有读和写的权限。怎么选择唯一文件名的?选择字符替换掉XXXXXX从而生成唯一文件。
此外,若把good_name设置成指针形式,则在创建临时文件的时候会发生段错误。因为数组形式的变量将保存在栈内存中,函数可对其进行替换更改,而指针形式只将指针存在了栈中,实际字符串在全局静态内存中,没法进行修改,所以出现错误。
参考:
《UNIX环境高级编程》
http://blog.csdn.net/armlinuxww/article/details/9341987