
2.9 常用的数据输入/输出函数
如图2.9.1所示,程序员可以给程序输入数据,程序处理后会返回一个输出。C语言通过函数库读取标准输入,然后通过对应函数处理将结果打印到屏幕上。前面我们学习了printf函数,理解了通过printf函数可以将结果输出到控制台窗口中。下面详细讲解标准输入函数scanf、getchar,以及打印到屏幕上的标准输出函数pintf、putchar。

图2.9.1 程序执行流程
2.9.1 scanf函数的原理
C语言未提供输入/输出关键字,其输入和输出是通过标准函数库来实现的。C语言通过scanf函数读取键盘输入,键盘输入又被称为标准输入。当scanf函数读取标准输入时,如果还没有输入任何内容,那么scanf函数会被卡住(专业用语为阻塞)。下面来看一个例子,如图2.9.2所示。

图2.9.2 阻塞函数程序
执行时输入20,然后回车,显示结果如图2.9.3所示。为什么第二个scanf函数不会被阻塞呢?其实是因为第二个scanf函数读取了缓冲区中的'\n',即scanf("%c",&c)实现了读取,打印其实输出了换行,所以不会阻塞。

图2.9.3 程序执行结果
下面介绍缓冲区原理。
缓冲区其实就是一段内存空间,分为读缓冲、写缓冲。C语言缓冲的三种特性如下。
(1)全缓冲:在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写操作。
(2)行缓冲:在这种情况下,当在输入和输出中遇到换行符时,将执行真正的I/O操作。这时,我们输入的字符先存放到缓冲区中,等按下回车键换行时才进行实际的I/O操作。典型代表是标准输入缓冲区(stdin)和标准输出缓冲区(stdout)。
(3)不带缓冲:也就是不进行缓冲,标准出错情况(stderr)是典型代表,这使得出错信息可以直接尽快地显示出来。
ANSI C(C89)要求缓存具有下列特征。
(1)当且仅当标准输入和标准输出不涉及交互设备时,它们才是全缓存的。
(2)标准出错绝不会是全缓存的。
如图2.9.2中的例子所示,我们向标准输入缓冲区中放入的字符为'20\n',输入'\n'(回车)后,scanf函数才开始匹配,scanf函数中的%d匹配整型数20,然后放入变量i中,接着进行打印输出,这时'\n'仍然在标准输入缓冲区(stdin)内,如果第二个scanf函数为scanf("%d",&i),那么依然会发生阻塞,因为scanf函数在读取整型数、浮点数、字符串(后面介绍数组时讲解字符串)时,会忽略'\n'(回车符)、空格符等字符(忽略是指scanf函数执行时会首先删除这些字符,然后再阻塞)。scanf函数匹配一个字符时,会在缓冲区删除对应的字符。因为在执行scanf("%c",&c)语句时,不会忽略任何字符,所以scanf("%c",&c)读取了还在缓冲区中残留的'\n'。
2.9.2 scanf函数的循环读取
如图2.9.4所示,如果想输入多个整数(每次输入都回车),让scanf函数读取并打印输出,那么我们需要一个while循环(不了解while循环时,可跳过本节,学完第3章以后再来看本节)。代码中为什么要加入fflush(stdin)函数呢?因为fflush函数具有刷新(清空)标准输入缓冲区的作用。如果我们输错了,输入的为字符型数据,那么scanf函数就无法匹配成功,scanf函数未匹配成功时其返回值为0,即ret的值为0,但这并不等于EOF,因为EOF的返回值为-1。当scanf函数无法匹配成功时,程序仍然会进入循环,这时会导致不断地重复打印。我们的实际执行结果如图2.9.5所示,最后我们按组合键Ctrl+Z,让scanf函数匹配失败,循环结束。读者可以自行尝试去除fflush(stdin),然后输入a,看一看效果。

图2.9.4 实现多次输入的程序

图2.9.5 程序的执行结果
针对VS 2013到VS 2017,需要按多次组合键Ctrl+Z来结束while循环,即需要按3次组合键Ctrl+Z才能让scanf函数出错返回-1。
接下来我们看一个读取字符串并打印对应字符串的大写字母的例子,如图2.9.6所示。原理是让scanf函数每次读取一个字符并打印,由于我们一次性输入一个字符串,然后回车,而scanf函数是循环匹配的,所以不能加fflush(stdin)。如果加了,那么会导致第一个字符匹配以后,后面的字符被清空。读者可以自行尝试一下。

图2.9.6 读取字符串并打印对应字符串的大写字母
图2.9.6中程序的执行结果如图2.9.7所示。

图2.9.7 程序的执行结果
2.9.3 多种数据类型混合输入
当我们让scanf函数一次读取多种类型的数据时,对于字符型数据要格外小心,因为当一行数据中存在字符型数据读取时,读取的字符并不会忽略空格和'\n'(回车符),所以使用方法如图2.9.8所示。编写代码时,我们需要在%d与%c之间加入一个空格。输入格式和输出效果如图 2.9.9所示,scanf函数匹配成功了4个成员,所以返回值为4,我们可以通过返回值来判断scanf函数匹配成功了几个成员,中间任何有一个成员匹配出错,后面的成员都会匹配出错。

图2.9.8 一次读取多种类型的数据

图2.9.9 输入格式和输出效果
由图2.9.9可以看出浮点数输出默认带了6位小数,看起来非常不美观,此时我们可以如例2.9.1所示那样修改代码,使得输出效果美观一些。
【例2.9.1】scanf函数读取混合类型输入。

程序中,l=%5.2f中的5代表输出的浮点数占5个空格的位置,2代表小数点后显示两位,这时输出结果如图2.9.10所示。

图2.9.10 例2.9.1中程序的输出结果
2.9.4 getchar函数介绍
使用getchar函数可以一次从标准输入读取一个字符,它等价于charc,scanf("%c",&c),语法格式如下:

通过例2.9.2中的代码我们可以用getchar函数读取一个字符。
【例2.9.2】getchar函数的使用。

getchar函数每次只能读取一个字符。例2.9.2中程序的执行结果如图2.9.11所示。

图2.9.11 例2.9.2中程序的执行结果
思考题:在上面的代码中,若在printf函数之后再加一个getchar函数,问getchar函数是否会被阻塞?
2.9.5 putchar函数介绍
输出字符型数据时使用putchar函数,其作用是向显示设备输出一个字符,语法格式如下:

其中,参数ch是要输出的字符,它既可以是字符型变量、整型变量,又可以是常量。输出字符'H'的代码如下:

例2.9.3的代码通过putchar函数实现变量、转义字符的打印。
【例2.9.3】putchar函数的使用。

例2.9.3中程序的输出结果如图2.9.12所示。

图2.9.12 例2.9.3中程序的输出结果
思考题:如果想让输出结果是ac而非bc,那么应该如何改动?
2.9.6 printf函数介绍
printf函数可以输出各种类型的数据,包括整型、浮点型、字符型、字符串型等,实际原理是printf函数将这些类型的数据格式化为字符串后,放入标准输出缓冲区,然后通过'\n'来刷新标准输出,并将结果显示到屏幕上。
语法如下:

printf函数根据format给出的格式打印输出到stdout(标准输出)和其他参数中。
字符串格式(format)由两部分组成:显示到屏幕上的字符和定义printf函数显示的其他参数。我们可以指定一个包含文本在内的format字符串,也可以是映射到printf的其他参数的“特殊”字符,如下列代码所示:

代码的输出如下:

其中,%s表示在该位置插入首个参数(一个字符串),%d表示第二个参数(一个整数)应该放在哪里。不同的%-codes表示不同的变量类型,也可以限制变量的长度。printf函数的具体代码格式如表2.9.1所示。
表2.9.1 printf函数的具体代码格式

位于%和格式化命令之间的一个整数被称为最小字段宽度说明符,通常会加上足够多的空格或0使输出足够长。如果想填充0,那么就在最小字段宽度说明符前面放置0。另外,也可以使用一个精度修饰符,精度修饰符根据使用的格式代码的不同通常有着不同的含义:
● 用%e、%E和%f精度修饰符指定想要的小数位数。例如,%5.2f会至少显示5位数字并带有2位小数的浮点数。
● 用%s精度修饰符简单地表示一个最大的长度,以补充句点前的最小字段长度。
printf函数的所有输出都是右对齐的,除非在%符号后放置了负号。例如,%-5.2f会显示5位字符、2位小数位的浮点数并且左对齐。
下面来看一个例子,如例2.9.4所示。
【例2.9.4】printf函数输出对齐。

执行结果如图2.9.13所示,可以看到整型数10在不加负号时靠右对齐,加负号时靠左对齐,%10s代表字符串共占用10个字符的位置。因为printf函数默认靠右对齐,所以"hello"字符串相对于左边的起始位置有5个空格的距离。掌握这些内容后,在后面做学生管理系统项目时,就会很容易掌握打印格式的控制。

图2.9.13 例2.9.4中程序的执行结果