C语言输入输出流和缓冲区
C语言缓冲区(缓存)详解
缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。 为什么要引入缓冲区比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。现在您基本明白了吧,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。 缓冲区的类型缓冲区 分为三种类型:全缓冲、行缓冲和不带缓冲。1) 全缓冲在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写。2) 行缓冲在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是标准输入(stdin)和标准输出(stdout)。3) 不带缓冲也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快地显示出来。ANSI C( C89 )要求缓存具有下列特征: 1.当且仅当标准输入和标准输出并不涉及交互设备时,它们才是全缓存的。2.标准出错决不会是全缓存的。但是,这并没有告诉我们如果标准输入和输出涉及交互作用设备时,它们是不带缓存的还是行缓存的,以及标准输出是不带缓存的,还是行缓存的。大部分系统默认使用下列类型的缓存: 1.标准出错是不带缓存的。2.如果是涉及终端设备的流,则它们是行缓存的;否则是全缓存的。我们经常要用到标准输入输出流,而ANSI C对stdin、stdout和stderr的缓存特征没有强行的规定,以至于不同的系统可能有不同的stdin、stdout和stderr的缓存特征。目前主要的缓存特征是:stdin和stdout是行缓存;而stderr是无缓存的。?缓冲区的大小如果我们没有自己设置缓冲区的话,系统会默认为标准输入输出设置一个缓冲区,这个缓冲区的大小通常是512个字节的大小。缓冲区大小由 stdio.h 头文件中的宏 BUFSIZ 定义,如果希望查看它的大小,包含头文件,直接输出它的值即可:- printf("%d", BUFSIZ);
缓冲区的大小是可以改变的,也可以将文件关联到自定义的缓冲区,详情可以查看 setvbuf()和 setbuf() 函数。
缓冲区的刷新(清空)下列情况会引发缓冲区的刷新: 1.缓冲区满时;2.行缓冲区遇到回车时;3.关闭文件;4.使用特定函数刷新缓冲区。结合缓冲区谈谈C语言getchar()、getche()、getch()的区别本文将用到C语言缓冲区的概念,如果您不了解缓冲区,请查看:C语言缓冲(缓存)三个函数的对比-- 缓冲区 头文件 回显getchar() 有缓冲区 stdio.h 有回显getch() 无缓冲区 conio.h 无回显getche() 无缓冲区 conio.h 有回显getchar()函数先来看一下getchar(),其原型为:int getchar(void);当程序调用getchar()函数时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar()函数才开始从键盘缓冲区中每次读入一个字符。也就是说,后续的getchar()函数调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才重新等待用户按键。通俗一点说,当程序调用getchar()函数时,程序就等着用户按键,并等用户按下回车键返回。期间按下的字符存放在缓冲区,第一个字符作为函数返回值。继续调用getchar()函数,将不再等用户按键,而是返回您刚才输入的第2个字符;继续调用,返回第3个字符,直到缓冲区中的字符读完后,才等待用户按键。下边的一个实例,会让你有深刻的体会:#includeint main(){ char c; //第一次调用getchar()函数 //程序执行时,您可以输入一串字符并按下回车键,按下回车键后该函数才返回 c=getchar(); //显示getchar()函数的返回值 printf("%c\n",c); //暂停 system("PAUSE"); while((c=getchar())!='\n') { printf("%c",c); } //暂停 system("PAUSE"); return 0;}
这段小代码很简单,并且在代码内部都有注释。
getchar()函数的执行就是采用了行缓冲。第一次调用getchar()函数,会让程序使用者(用户)输入一行字符并直至按下回车键 函数才返回。此时用户输入的字符和回车符都存放在行缓冲区。再次调用getchar()函数,会逐步输出行缓冲区的内容。请再看下面一个例子:#includeint main(){ char ch1; char ch2; ch1 = getchar(); ch2 = getchar(); printf("%d %d", ch1, ch2); return 0;}
程序的本意很简单,就是从键盘读入两个字符,然后打印出这两个字符的ASCII码值。可是执行程序后会发现出了问题:当从键盘输入一个字符后,就打印出了结果,根本就没有输入第二个字符程序就结束了。例如用户输入字符’a', 打印结果是97,10。这是为什么呢?
getchar()函数是从输入流缓冲区中读取数据的,而不是从键盘(终端)缓冲区读取。当读取遇到回车(\n)结束时,这个'\n'会一起读入到输入流缓冲区的,所以第一次接收输入时取走字符后会留下字符\n,这样第二次getchar()直接从缓冲区中把\n取走了,显然读取成功了,所以不会再从终端读取!其实这里的10恰好是回车符!这就是为什么这个程序只执行了一次输入操作就结束的原因!
getch()和getche()函数 在TC2.0时代,C程序员总是喜欢在程序末尾加上getch(),来实现程序运行完了暂停不退出的效果。如果不这样做,在TC2.0的环境中Ctrl+F9编译并运行后会立即退出程序,根本来不及看到结果。这时如果要看结果,就要按Alt+F5回到DOS环境中去,很麻烦。而如果在程序的结尾加上一行getch();语句,就可以省掉回DOS看结果这个步骤,因为程序运行完了并不退出,而是在程序最后把屏幕停住了,按任意键才退出程序。实际上,getch()的作用是从键盘接收一个字符,且不带回显。就是说,你按了一个键后它并不在屏幕上显示你按的什么,而继续运行后面的代码,所以在C语言中可以用它来实现“按任意键继续”的效果,即程序中遇到getch();语句,就会停下来,等你按任意键,它接收了这个字符键后再继续执行后面的代码。getche()和getch()很相似,它也需要引入头文件conio.h,它们之间的区别就在于:getch()无回显,getche()有回显。请看下面的例子:#include#include void main(){ char ch; int i; for(i=0;i<5;i++) { ch=getch(); printf("%c",ch); }}
#include#include void main(){ char ch='*'; while(ch=='*') { printf("\n按 * 继续循环,按其他键退出!"); ch=getch(); } printf("\n退出程序!");}
你可以在这个循环体中添加你想要的功能,程序中按*继续循环,其他任意键退出,而且利用getch()无回显的特性,不管你按什么键,都不会在屏幕上留下痕迹,使你的界面达到美观效果。
还有getchar是很值得研究的:getchar()是stdio.h中的库函数,它的作用是从stdin流(标准输入流)中读入一个字符,也就是说,如果stdin有数据的话不用输入它就可以直接读取了。而getch()和getche()是conio.h中的库函数,它的作用是从键盘接收字符。与前面两个函数的区别在于: getchar()函数等待输入直到按回车才结束(前提是缓冲区没有数据),回车前的所有输入字符都会逐个显示在屏幕上。但只有第一个字符作为函数的返回值。#include#include void main(){ char c; // 从键盘读入字符直到回车结束 //getchar()在这里它只返回你输入字符串的第一个字符,并把返回值赋值给c c=getchar(); // 显示输入的第一个字符 putchar(c);}
看到这个程序,相信你肯定会有疑问。这个就是从缓冲区中读取字符的例子。第一次getchar()时,确实需要人工的输入,但是如果你输了多个字符,以后的getchar()再执行时就会直接从缓冲区中读取了。
#include#include void main(){ char c; // 每个getchar()依次读入一个字符 while ((c=getchar())!='\n') printf("%c",c); // 按照原样输出}
#define NULL 0#define EOF (-1)#define BUFSIZ 1024#define OPEN_MAX 20 // 一次打开的最大文件数// 定义FILE结构体typedef struct _iobuf {int cnt; // 剩余的字符,如果是输入缓冲区,那么就表示缓冲区中还有多少个字符未被读取char *ptr; // 下一个要被读取的字符的地址char *base; // 缓冲区基地址int flag; // 读写状态标志位int fd; // 文件描述符} FILE;extern FILE _iob[OPEN_MAX];#define stdin (&_iob[0]) // stdin 的文件描述符为0#define stdout (&_iob[1]) // stdout 的文件描述符为1#define stderr (&_iob[2]) // stdout 的文件描述符为2enum _flags { _READ =01, // 读文件 _WRITE =02, // 写文件 _UNBUF =04, // 无缓冲 _EOF = 010, // 文件结尾EOF _ERR = 020 // 出错};int _fillbuf(FILE *); // 函数声明,填充缓冲区int _flushbuf(int, FILE *); // 函数声明,刷新缓冲区#define feof(p) ((p)->flag & _EOF) != 0)#define ferror(p) ((p)->flag & _ERR) != 0)#define fileno(p) ((p)->fd)#define getc(p) (--(p)->cnt >= 0 \ ? (unsigned char) *(p)->ptr++ : _fillbuf(p))#define putc(x,p) (--(p)->cnt >= 0 \ ? *(p)->ptr++ = (x) : _flushbuf((x),p))#define getchar() getc(stdin)#define putcher(x) putc ((x), stdout)
看吧,我们经常使用的 NULL、EOF、feof()、getc() 等都是 stdio.h 中定义的宏。
注意:一个长的 #define 语句可以用反斜杠(\)分成多行。下面说一下如果控制缓冲区。
我们知道,当我们从键盘输入数据的时候,数据并不是直接被我们得到,而是放在了缓冲区中,然后我们从缓冲区中得到我们想要的数据 。如果我们通过setbuf()或setvbuf()函数将缓冲区设置10个字节的大小,而我们从键盘输入了20个字节大小的数据,这样我们输入的前10个数据会放在缓冲区中,因为我们设置的缓冲区的大小只能够装下10个字节大小的数据,装不下20个字节大小的数据。那么剩下的那10个字节大小的数据怎么办呢?暂时放在了输入流中。请看下图: 上面的箭头表示的区域就相当是一个输入流,红色的地方相当于一个开关,这个开关可以控制往深绿色区域(标注的是缓冲区)里放进去的数据,输入20个字节的数据只往缓冲区中放进去了10个字节,剩下的10个字节的数据就被停留在了输入流里!等待下去往缓冲区中放入!接下来系统是如何来控制这个缓冲区呢?再说一下 FILE 结构体中几个相关成员的含义:cnt //剩余的字符,如果是输入缓冲区,那么就表示缓冲区中还有多少个字符未被读取ptr//下一个要被读取的字符的地址base//缓冲区基地址在上面我们向缓冲区中放入了10个字节大小的数据,FILE结构体中的 cnt 变为了10 ,说明此时缓冲区中有10个字节大小的数据可以读,同时我们假设缓冲区的基地址也就是 base 是0x00428e60 ,它是不变的 ,而此时 ptr 的值也为0x00428e60 ,表示从0x00428e60这个位置开始读取数据,当我们从缓冲区中读取5个数据的时候,cnt 变为了5 ,表示缓冲区还有5个数据可以读,ptr 则变为了0x0042e865表示下次应该从这个位置开始读取缓冲区中的数据 ,如果接下来我们再读取5个数据的时候,cnt 则变为了0 ,表示缓冲区中已经没有任何数据了,ptr 变为了0x0042869表示下次应该从这个位置开始从缓冲区中读取数据,但是此时缓冲区中已经没有任何数据了,所以要将输入流中的剩下的那10个数据放进来,这样缓冲区中又有了10个数据,此时 cnt 变为了10 ,注意了刚才我们讲到 ptr 的值是0x00428e69 ,而当缓冲区中重新放进来数据的时候这个 ptr 的值变为了0x00428e60 ,这是因为当缓冲区中没有任何数据的时候要将 ptr 这个值进行一下刷新,使其指向缓冲区的基地址也就是0x0042e860这个值!因为下次要从这个位置开始读取数据!在这里有点需要说明:当我们从键盘输入字符串的时候需要敲一下回车键才能够将这个字符串送入到缓冲区中,那么敲入的这个回车键(\r)会被转换为一个换行符\n,这个换行符\n也会被存储在缓冲区中并且被当成一个字符来计算!比如我们在键盘上敲下了123456这个字符串,然后敲一下回车键(\r)将这个字符串送入了缓冲区中,那么此时缓冲区中的字节个数是7 ,而不是6。缓冲区的刷新就是将指针 ptr 变为缓冲区的基地址 ,同时 cnt 的值变为0 ,因为缓冲区刷新后里面是没有数据的!C语言为指针动态分配内存C语言程序员要严防内存泄漏,这个“内存泄漏”就是由动态内存分配引起的。指针是C语言和其它语言的最大区别,也是很多人不能跨入C语言的一道门槛。既然指针是这么一个“危险”的坏东西,干吗不取消它呢?其实指针本身并没有好坏,它只是一种操作地址的方法,学会了便可以发挥其它语言难以匹敌的功能,没学会的话,只能做其它语言的程序员,也同样发挥你的光和热。变量和数组可以通过指针来转换“int *x”中的x究竟是不是数组?光看这一句无法确定,因为它既可表示单个变量内容,也可表示数组。请理解下面的例子:
#includeint main(void){ int *num = NULL; int *x, y[] = { 12, 22,32}, z = 100; //下面演示,指针既可充当变量、也可充当数组 x=&z; //整型变量的地址赋给x printf("*x=%d, x[0]=%d\n", *x, x[0]); x = y; //数组的地址赋给x printf("*x=%d, x[ 0]=%d, x[ 1]=%d, x[2]=%d\n", *x, x[0], x[1], x[2]); x = y + 1; //数组的第二位地址赋给x printf("*x=%d, x[-1]=%d, x[ 0]=%d, x[1]=%d\n", *x, x[-1], x[0], x[1]); x = y + 2; //数组的第三位地址赋给x printf("*x=%d, x[-2]=%d, x[-1]=%d, x[0]=%d\n", *x, x[-2], x[-1], x[0]); return 0;}
运行结果:
*x=100, x[0]=100*x=12, x[ 0]=12, x[ 1]=22, x[2]=32*x=22, x[-1]=12, x[ 0]=22, x[1]=32*x=32, x[-2]=12, x[-1]=22, x[0]=32 动态分配内存前面讲到的指针,基本上将已经定义好的变量的地址赋给指针变量,现在要学的是向操作系统申请一块新的内存。申请到的内存,必须在某个地方手动释放,因此下面2个函数必须配对使用。malloc()和free(),都是标准函数,在stdlib.h中定义。根据不同的电脑使用状况,申请内存有可能失败,失败时返回NULL,因此,动态申请内存时一定要判断结果是否为空。malloc()的返回值类型是“void *”,因此,不要忘记类型转换。(许多人都省略了。)#include#include #include int main(void){ char *p ; p = (char *)malloc(60 * sizeof(char)) ; if (p == NULL) { //这个判断是必须的 printf("内存分配出错!"); exit(1); } strcpy(p, "http://see.xidian.edu.cn/cpp/u/jiaocheng/\n"); //不要忘记给新内存赋值 printf("%s", p); free(p); //过河一定要拆桥 p = NULL ; //释放后的指针置空,这是非常好的习惯,防止野指针。 return 0;}
运行结果:
http://see.xidian.edu.cn/cpp/u/jiaocheng/ 隐蔽的内存泄漏内存泄漏主要有以下几种情况:1.内存分配未成功,却使用了它。2.内存分配虽然成功,但是尚未初始化就引用它。3.内存分配成功并且已经初始化,但操作越过了内存的边界。4.忘记了释放内存,造成内存泄露。5.释放了内存却继续使用它。下面的程序造成内存泄漏,想想错在何处?如何修改?#include#include int main(void){ int *p, i; p = (int *)malloc(6 * sizeof(int)) ; if (p == NULL) { //判断是否为空 printf("内存分配出错!"); exit(1); } for (i=0; i<6; i++) { p++; *p = i; printf("%2d", *p); } printf("\n"); free(p); //这句运行时出错 return 0;}
对动态内存的错误观念有人对某一只在函数内使用的指针动态分配了内存,用完后不释放。其理由是:函数运行结束后,函数内的所有变量全部消亡。这是错误的。动态分配的内存是在“堆”里定义,并不随函数结束而消亡。有人对某动态分配了内存的指针,用完后直接设置为NULL。其理由是:已经为NULL了,这就释放了。这也是错误的。指针可以任意赋值,而内存并没有释放;相反,内存释放后,指针也并不为NULL。