第三章 格式化输入/输出 🚀
scanf函数和printf函数是C语言编程中使用最频繁的两个函数,它们用来格式化输入和输
出。
3.1 printf 函数🚀
printf函数被设计用来显示 格式串 ( format string )的内容,并且在该串中的指定位置插
入可能的值。调用 printf 函数时必须提供格式串,格式串后面的参数是需要在显示时插入到
该串中的值:
printf(格式串 , 表达式 1, 表达式 2, ...);
显示的值可以是常量、变量或者更加复杂的表达式。调用 printf 函数一次可以打印的值的个数
没有限制。
格式串包含普通字符和转换说明 ( conversion specification ),其中转换说明以字符 % 开头。转换说明是用来表示打印过程中待填充的值的占位符。跟随在字符% 后边的信息指定了把数值从内部形式(二进制)转换 成打印形式(字符)的方法,这也就是“转换说明”这一术语的由来。例如,转换说明%d 指定 printf 函数把 int 型值从二进制形式转换成十进制数字组成的字符串,转换说明%f 对 float 型值也进行类似的转换。
注意:
C语言编译器不会检测格式串中转换说明的数量是否和输出项的数量相匹配。 下
面这个 printf 函数调用所拥有的转换说明的数量就多于要显示的值的数量:
printf("%d %d\ n", i); /*** WRONG ***/
printf函数将正确显示变量i的值,接着显示另一个(无意义的)整数值。 函数调用
带有太少的转换说明也会出现类似的问题:
printf("%d\n", i, j); /*** WRONG ***/
在这种情况下,printf函数会显示变量i的值,但是不显示变量j的值。
此外, C 语言编译器也不检测转换说明是否适合要显示项的数据类型。如果程序
员使用不正确的转换说明,程序将会简单地产生无意义的输出。思考下面的 printf
函数调用, 其中int型变量i和float型变量x的顺序放置错误:
printf("%f %d\n", i, x); /*** WRONG ***/
因为 printf 函数必须服从于格式串,所以它将如实地显示出一个 float 型值,接着是
一个 int 型值。可惜这两个值都将是无意义的。
3.1.1 转换说明🚀
转换说明可以用%m.pX格式或%-m.pX格式
最小栏宽 ( minimum field width ) m 指定了要显示的最少字符数量。如果要显示的数值所
需的字符数少于 m ,那么值在字段内是右对齐的。(换句话说,在值前面放置额外的空格。)例如,转换说明%4d 将以 · 123 的形式显示数 123 (本章用符号 · 表示空格字符)。如果要显示的值所需的字符数多于m ,那么字段宽度会自动扩展为所需的尺寸。因此,转换说明 %4d 将以 12345的形式显示数12345 ,而不会丢失数字。在 m 前放上一个负号会导致左对齐;转换说明 %-4d 将以123· 的形式显示 123 。
精度 ( precision ) p 的含义很难描述,因为它依赖于 转换说明符 ( conversion specifier ) X 的选择。X 表明在显示数值前需要对其进行哪种转换。对数值来说最常用的转换说明符有以下几个。
编写程序时无法预知数的大小或者数值变化范围很大的情况下,说明符 g 对于数的显示是特
别有用的。在用于显示大小适中的数时,说明符 g 采用定点十进制形式。但是,在显示非常大或非常小的数时,说明符g 会转换成指数形式以便减少所需的字符数。
3.1.2 转义序列🚀
格式串中常用的代码\n被称为转义序列(escape sequence)。转义序列( 7.3节)使字符串包含一些特殊字符而不会使编译器引发问题,这些字符包括非打印的(控制)字符和对编译器有 特殊含义的字符(如 " )
当这些转义序列出现在 printf 函数的格式串中时,它们表示在显示中执行的操作。在大多
数机器上,输出 \a 会产生一声鸣响,输出 \b 会使光标从当前位置回退一个位置,输出 \n 会使光
标跳到下一行的起始位置,输出 \t 会把光标移动到下一个制表符的位置。
附带提一下,不能在字符串中只放置单独一个字符 \ ,编译器将认为它是一个转义序列的开
始。为了显示单独一个字符 \ ,需要在字符串中放置两个 \ 字符:
printf("\\"); /* prints one \ character */
3.2 scanf 函数🚀
scanf函数转换说明的用法和printf函数转换说明的用法本质上是一样的。
scanf 函数调用中像 "%d%d%f%f" 这样“紧密压缩”的格式串是很普遍的,而printf 函数的格式串很少有这样紧挨着的转换说明。
3.2.1 scanf 函数的工作方法🚀
实际上scanf函数可以做的事情远远多于目前为止已经提到的这些。scanf 函数本质上是一
种“模式匹配”函数,试图把输入的字符组与转换说明相匹配。
像printf函数一样,scanf函数是由格式串控制的。 调用时,scanf函数从左边开始处理字符串中的信息。对于格式串中的每一个转换说明,scanf函数从输入的数据中定位适当类型的项,并在必要时跳过空格。然后,scanf函数读入数据项,并且在遇到不可能属于此项的字符时停止。如果读入数据项成功,那么scanf函数会继续处理格式串的剩余部分;如果某一项不能成功读入,那么scanf函数将不再查看格式串的剩余部分(或者余下的输入数据)而立即返回。
在寻找数的起始位置时,scanf函数会忽略空白字符 ( white-space character ,包括空格符、
水平和垂直制表符、换页符和换行符)。因此,我们可以把数字放在同一行或者分散在几行内输入。考虑下面的scanf 函数调用:
scanf("%d%d%f%f", &i, &j, &x, &y);
假设用户录入 3 行输入:
1
-20 .3
-4.0e3
scanf 函数会把它们看成是一个连续的字符流:
·· 1¤-20 ··· .3¤ ··· -4.0e3¤
(这里使用符号 · 表示空格符,用符号 ¤ 表示换行符。)因为 scanf 函数在寻找每个数的起始位置时会跳过空白字符,所以它可以成功读取这些数。在接下来的图中,字符下方的s 表示此项被跳过,而字符下面的r 表示此项被读取为输入项的一部分:
scanf函数“忽略”了最后的换行符,实际上没有读取它。这个换行符将是下一次scanf函数调
用的第一个字符。
scanf 函数遵循什么规则来识别整数或浮点数呢?在要求读入整数时, scanf 函数首先寻找正号或负号,然后读取数字直到读到一个非数字时才停止。当要求读入浮点数时,scanf函数
会寻找一个正号或负号(可选),随后是一串数字(可能含有小数点),再后是一个指数(可选)。指数由字母e (或者字母 E )、可选的符号和一个或多个数字构成。在用于 scanf 函数时,转换说明%e 、 %f 和 %g 是可以互换的,这 3 种转换说明在识别浮点数方面都遵循相同的规则。
当scanf函数遇到一个不可能属于当前项的字符时,它会把此字符“放回原处”,以便在扫描下一个输入项或者下一次调用scanf函数时再次读入。 思考下面(公认有问题的) 4 个数的排列:
3.2.2 格式串中的普通字符🚀
通过编写含有普通字符和转换说明的格式串能更进一步地理解模式匹配的概念。处理格式
串中的普通字符时,scanf函数采取的动作依赖于这个字符是否为空白字符。
空白字符。 当在格式串中遇到一个或多个连续的空白字符时,scanf函数从输入中重复
读空白字符直到遇到一个非空白字符(把该字符“放回原处”)为止。 格式串中空白字
符的数量无关紧要,格式串中的一个空白字符可以与输入中任意数量的空白字符相匹
配。(附带提一下,在格式串中包含空白字符并不意味着输入中必须包含空白字符。格
式串中的一个空白字符可以与输入中 任意 数量的空白字符相匹配,包括零个。)
其他字符。 当在格式串中遇到非空白字符时,scanf函数将把它与下一个输入字符进行
比较。 如果两个字符相匹配,那么 scanf 函数会放弃输入字符而继续处理格式串。如果
两个字符不匹配,那么 scanf 函数会把不匹配的字符放回输入中,然后异常退出,而不
进一步处理格式串或者从输入中读取字符。
例如,假设格式串是"%d/%d"。如果输入是:
· 5/ · 96
在寻找整数时, scanf 函数会跳过第一个空格,把 %d 与 5 相匹配,把 / 与 / 相匹配,在寻找下一个整数时跳过一个空格,并且把%d 与 96 相匹配。另一方面,如果输入是:
· 5 · / · 96
scanf 函数会跳过一个空格,把 %d 与 5 相匹配,然后试图把格式串中的 / 与输入中的空格相匹配。但是二者不匹配,所以scanf 函数把空格放回原处,把字符· / · 96 留给下一次 scanf 函数调用来读取。为了允许第一个数后边有空格,应使用格式串"%d /%d" 。
3.2.3 易混淆的 printf 函数和 scanf 函数🚀
问与答(很重要)🚀
*问:转换说明%i也可以用于读写整数。%i和%d之间有什么区别?(p.27)
* 答:在 printf 格式串中使用时,二者没有区别。但是,在 scanf 格式串中 %d 只能与十进制(基数为 10 )形式的整数相匹配,而%i 则可以匹配用八进制(基数为 8 )、十进制或十六进制(基数为 16 )表示的整数。如果输入的数有前缀0 (如 056 ),那么 %i 会把它作为八进制数( 7.1 节)来处理;如果输入的数有前缀0x 或 0X (如 0x56 ),那么 %i 会把它作为十六进制数( 7.1 节)来处理。如果用户意外地将0 放在数的开始处,那么用 %i 代替 %d 读取数可能有意想不到的结果。由于这是一个陷阱,所以建议坚持采用%d 。
问:如果printf函数将%作为转换说明的开始,那么如何显示字符%呢?(选读)
* 答:如果 printf 函数在格式串中遇到两个连续的字符 % ,那么它将显示出一个字符 % 。例如,语句
printf("Net profit: %d%%\n", profit);
可以显示出
Net profit: 10%
* 问:转义序列 \t 会使 printf 函数跳到下一个水平制表符处。如何知道水平制表符到底跳多远呢? ( p.29 )
* 答:不可能知道。打印 \t 的效果不是由 C 语言定义的,而是依赖于所使用的操作系统。水平制表符之间的距离通常是8 个字符宽度,但 C 语言本身无法保证这一点。
问:如果要求读入一个数,而用户却录入了非数值的输入,那么scanf函数会如何处理?
* 答:请看下面的例子:
printf("Enter a number: ");
scanf("%d", &i);
假设用户录入了一个有效数,后边跟着一些非数值的字符:
Enter a number: 23foo
这种情况下,scanf函数读取2和3,并且将23存储在变量i中,而剩下的字符(foo)则留给下一次scanf函数调用(或者某些其他的输入函数)来读取。 另一方面,假设输入从开始就是无效的:
Enter a number: foo
这种情况下,没有值会被存储到变量i中,字符foo会留给下一次scanf函数调用。
如何处理这种糟糕的情况呢?后面将看到检测 scanf 函数调用是否成功( 22.3 节)的方法。如
果调用失败,可以终止或者尝试恢复程序,可能的方法是丢掉有问题的输入并要求用户重新输入。(在第22 章结尾的“问与答”部分会讨论有关丢弃错误输入的方法。)
问:我不能理解scanf函数如何把字符“放回原处”并在以后再次读取。(p.31)
答:我们知道,用户从键盘输入时,程序并没有读取输入,而是把用户的输入放在一个隐藏的缓冲区中,由scanf 函数来读取。 scanf 函数把字符放回到缓冲区中供后续读取是非常容易的。第 22 章将会更详细地讨论输入缓冲。
问:如果用户在两个数之间加入了标点符号(如逗号),scanf函数将如何处理?
答:先来看一个简单的例子。假设我们想用 scanf 函数读取一对整数:
printf("Enter two numbers: ");
scanf("%d%d", &i, &j);
如果用户录入
4,28
scanf 函数将读取 4 并且把它存储在变量 i 中。在寻找第二个数的起始位置时, scanf 函数遇到了逗号。因为数不能以逗号开头,所以scanf 函数立刻返回,而把逗号和第二个数留给下一次scanf 函数调用。
当然,如果能确定数与数之间始终用逗号进行分割,我们可以很容易地解决这个问题,只要在格式串中添加逗号即可:
printf("Enter two numbers, separated by a comma: ");
scanf("%d,%d", &i, &j);