=============================开发环境=============================
操作系统:Microsoft Windows 10 专业版 10.0.16299
CPU:Intel(R) Core(TM) i3-4130 CPU @ 3.40GHz
内存:金士顿 DDR3 1600MHz 4GB*2
显卡:NVIDIA GeForce GT 1030
所用IDE及工具链:Code::Blocks 16.01+MinGW-w64
==================================================================
(本文中大部分例程来自C Primer Plus中相关例程)
学习C语言的同学,应该对getchar()与putchar()函数都不陌生
这里通过一个简单的例程来复习一下这两个函数的基本用法
[mw_shl_code=c,true]/*p217 echo.c*/
#include <stdio.h>
int main(void)
{
char ch;
while ((ch = getchar()) != '#')
putchar(ch);
return 0;
}
[/mw_shl_code]
该程序获取从键盘输入的字符,并把这些字符发送到屏幕上。程序使用while循环,当读到#字符时停止。
程序运行结果
但同样的程序在一些古老的编译器/系统中可能会出现问题,比如说出现下图的情况
老式系统中该程序可能的运行结果
从《C Primer Plus》书中可知:
像这样会先用户输入的字符后立即重复打印该字符是属于无缓冲(或直接)输入。对于该例,大部分系统在用户按下Enter键之前不会重复打印刚输入的字符,这种输入形式属于缓冲输入。用户输入的字符被收集并储存在一个被称为缓冲区(buffer)的临时存储区,按下Enter键后,程序才可使用用户输入的字符。
ANSI C和之后的C标准都注明:C语言中的输入是缓冲的,但早期的未规范的C语言程序中有可能出现如上例所示的无缓冲输入
在现行的C标准中,没有提供调用无缓冲输入的标准方式。
也就是说,在我们现在编写的符合规范的C语言程序,都是要使用缓冲区的。
为了更好的理解
缓冲区的概念,下面我们看一个简单的例程:
[mw_shl_code=c,true]#include <stdio.h>
//例程1
int main(void)
{
int a[5];
for(int i=0;i<5;i++)
scanf("%d",&a);
for(int i=0;i<5;i++)
printf("%d ",a);
return 0;
}
[/mw_shl_code]
让我们看一下这个程序的一个简单运行范例:
我实际输入的是:123[空格]123[空格]423[空格]234[空格]4[回车]
那么,在我输入回车之前,“123[空格]123[空格]423[空格]234[空格]4”这一段数据就是保存在缓冲区中
然后再由scanf函数一个一个将其中的数据调用出来。
不过,我们要注意的是,实际上,回车所输入的换行符“\n”也是会被输入缓冲区的。
在某些应用当中需要特别注意,这里限于篇幅不深入讨论
讲了这么多,我们再回头来看看本篇文章中的第一个例程:
[mw_shl_code=c,true]/*p217 echo.c*/
#include <stdio.h>
int main(void)
{
char ch;
while ((ch = getchar()) != '#')
putchar(ch);
return 0;
}
[/mw_shl_code]
这个程序虽然完成了“从键盘输入字符再显示出来”的任务,但很明显完成的并不完美。
特别是,在当我们输入'#'字符后,后面的内容便不再被程序录入。
那么,我们该如何解决这样的问题呢?让我们来看看C语言给出的解决方案:
首先,让我们了解一下C语言中文件、流和键盘输入的关系
(以下摘自《C Primer Plus》)
C是一门强大、灵活的语言,有许多用于打开、读取、写入和关闭文件的库函数。从较低层面上,C可以使用主机操作系统的基本文件工具直接处理文件,这些直接调用操作系统的函数被称为底层I/O。由于计算机系统各不相同,所以不可能为普通的底层i/o函数创建标准库,ANSI C也不打算这样做。然而下哦那个较高层面上,C还可以通过标准I/O包来处理文件。这设计创建用于处理文件的标准模型和一套标准I/O函数。在这一层面上,具体的C实现负责处理不同系统的差异,以便用户使用统一的界面。
上面讨论的差异指的是什么?例如,不同的系统储存文件的方式不同。有些系统把文件的内容储存在一处,而文件相关的信息储存在另一处;有些系统在文件中创建一份文件描述。在处理文件方面,有些系统使用单个换行符标记行末尾,而其他系统可能使用回车符和换行符的组合来表示行末尾。有些系统用最小子节来衡量文件的大小,有些系统则以字节块的大小来衡量。
如果使用标准I/O包,就不用考虑这些差异。因此,可以用if (ch == '\n')检查换行符。即使系统实际用的是回车符和换行符的组合来标记行末尾,I/O函数会在两种表示法之间相互转换。
从概念上看,C程序处理的是流而不是直接处理文件。流是一个实际输入或输出映射的理想化数据流。这意味着不同属性和不同种类的输入,由属性更统一的流来表示。于是,打开文件的过程就是把流与文件相关联,而且读写都通过流来完成。
我们平时在C语言中的学习中,使用的最多的大概就是表示键盘输入的stdin流和屏幕输出的stdout流了
既然我们的键盘输入和屏幕输出在C语言内部的“地位”与指代文件流一样,都是“流”
那么,程序读文件时要能检测文件的末尾才知道应在何处停止。因此,C的输入函数内置了文件结尾检测器
既然键盘输入和文件输入是等价的,那么我们能不能在C语言中调用文件结尾检测器来结束键盘输入,
解决例程中出现的问题呢?
(未完待续)
以上部分编辑于
2018-1-28 11:46
以下部分补充于
2018-2-1
万分抱歉,由于近期事务繁忙,笔记未能及时更新
这一次,就当作是“番外加更”,同大家再讲讲关于C语言缓冲区的一些问题
之所以想在回归更新笔记的“主线”之前先更新这样的一篇“番外”
主要是在一个大学生编程相关的qq群中,有一位同学问了我他/她的程序有什么问题
我看了看这个程序,发现这是个讲解缓冲区的不错的例子
于是在争取了原来那位同学的许可下,我得以将这个程序作为例程展现给大家
这是那位同学一开始的程序:
[mw_shl_code=c,true]#include <stdio.h>
struct Lovers
{
char sex;
double height;
};
int main(void)
{
int N, i;
struct Lovers s[10];
scanf("%d", &N);
for (i = 0; i < N; i++)
{
scanf("%c %lf", &s
.sex, &s.height);
}
for (i = 0; i < N; i++)
{
if (s.sex == 'M')
printf("%.2f", s.height / 1.09);
if (s.sex == 'F')
printf("%.2f", s.height * 1.09);
if (i != N)
printf("\n");
}
return 0;
}[/mw_shl_code]
从代码中可以看出这是一个求最佳伴侣身高的程序,这名同学的意愿我们也可以分析的很清楚
从一开始的scanf函数中输入人数,然后根据人数进行循环
在每一次循环中,用scanf函数输入每个人的性别和身高信息,最后再使用printf函数和循环输出符合每个人的最佳伴侣身高信息
可是,这个程序的运行结果却并不能完美符合这位同学的心意
程序运行结果如下图:
我们可以推测出来,这位同学是想输入两个人的数据,但是在他/她刚刚输入完第一个人的数据并按下回车后
程序却输出了意料之外的结果
出现这种状况的原因是什么呢?让我们打开IDE的调试模式一探究竟
(注意,此处最好使IDE处于“调试(debug)”状态)
上图中为Code::Blocks17.12中与Microsoft Visual Studio 2017中切换调试(debug)与发行(release)模式的地点
(可能会与你的电脑稍有差异)
我们在程序的第十六行设置断点,以调试模式执行程序
可以看到我们的程序运行停止在了断点处
此时我们可以查看内存中各变量具体的值
我们可以惊讶的发现,结构体数组s[]当中s[0]的数据与我们所预计的并不一样
sex中所储存的单个字符的值竟然是'\n',而height中所储存的身高值更是看起来就像是内存中未初始化的数据
这是为什么呢?
原来,在我们输入“2[回车]”的时候
不仅'2'进入了缓冲区,“回车”所代表的'\n'同样也进入了缓冲区
在我们平时使用scanf函数时常使用的指定转换符%d会跳过空格和换行符
但%c可不是这个样子的
程序12到15行的for循环中的scanf函数第一次运行时
scanf函数中的%c读取到一个换行符,并将其存入s[0].sex中,而由于scanf函数读取到了换行符,函数运行结束
因此实际上s[0].height的值仍然是内存中未初始化的初值
换个角度看,for循环中第一次运行的scanf在我们认为其运行之前就已经运行结束了
实际上我们感觉到运行的只有i=1时的scanf函数
那么,如何避免这个问题呢?
在这里放上一个比较简单的解决方案
[mw_shl_code=c,true]#include <stdio.h>
struct Lovers
{
char sex;
double height;
};
int main(void)
{
int N, i;
char ch;
struct Lovers s[10];
scanf("%d", &N);
ch = getchar();
for (i = 0; i < N; i++)
{
scanf("%c %lf", &s.sex, &s.height);
ch = getchar();
}
for (i = 0; i < N; i++)
{
if (s.sex == 'M')
printf("%.2f", s.height / 1.09);
if (s.sex == 'F')
printf("%.2f", s.height * 1.09);
if (i != N)
printf("\n");
}
return 0;
}[/mw_shl_code]
大家可以结合我上面对缓冲区的一部分讲解,理解一下这个程序的原理
以上部分最后编辑于
2018-2-1 12:36
以下部分补充于
2018-2-8
接着上一次断掉的话题,我们该如何在C语言中调用文件结尾检测器来结束键盘输入,
解决例程中出现的问题呢?
在《C Primer Plus》中,对检测文件末尾有着这样的叙述:
无论操作系统实际使用何种方法检测文件结尾,在c语言中,用getchar()读取文件检测到文件结尾时将返回一个特殊的值,即EOF(end of file的缩写)。scanf()函数检测到文件结尾时也返回EOF。通常,EOF定义在stdio.h文件中
对于这一点的应用,我们可以看下面的例程:
[mw_shl_code=c,true]/* P221 echo_eof.c -- 重复输入,直到文件结尾*/
#include <stdio.h>
int main(void)
{
int ch;
while ((ch = getchar()) != EOF)
putchar(ch);
return 0;
}[/mw_shl_code]
其运行结果如下:
在windows的命令提示符界面中,要用键盘输入文件结束标记,需要同时按下Ctrl+Z
此时在屏幕上所显示的就是^Z,但实际上在程序中这相当于输入了'EOF'
于是程序停止
通过类似这样的方法,我们就可以做到在不影响正常输入的情况下,通过模拟文件结束标记来进行输入的结束
那么,我们现在已经知道如何通过模拟文件结束标记来结束程序了
但当我们想要让
现有的程序将结果输出到某个文件中
或是从某个文件中读取输入数据
我们又该怎么办呢
这时,就是
操作系统所带有的
重定向功能派上用场的时候了
那么,重定向功能又该如何使用呢?
把上一节的程序作为例子,我们来测试一下
首先,我们在上节例程的文件夹下新建一个名为words的文本文档
并在其中输入如下图中内容保存
之后,我们通过命令提示符方式调用程序,并使用重定向符'<'来指定输入为words.txt文件
程序运行结果如下:
可以看到,程序把words.txt中我们所输入的一段话当做了stdin流,即标准输入流中的输入
同样的方法,我们也可以用重定向符'>'来指定一个文件用来输出
例如:
程序运行完成之后,在其文件夹下生成了output.txt文件
其内容为
正是我们刚才所输入的内容