C 语言中的 printf() 和 scanf() 简介

printf() 函数和 scanf() 函数能让用户可以与程序交流,它们是输出/输入函数,或简称为 I/O 函数。它们不仅是 C 语言中的 I/O 函数,而且是最多才多艺的函数。过去,这些函数和 C 库的一些其他函数一样,并不是 C 语言定义的一部分。最初,C 把输入/输出的实现留给了编译器的作者,这样可以针对特殊的机器更好地匹配输入/输出。后来,考虑到兼容性的问题,各编译器都提供不同版本的 printf() 和 scanf()。尽管如此,各版本之间偶尔有一些差异。C90 和 C99 标准规定了这些函数的标准版本,本文亦遵循这一标准。

虽然 printf() 是输出函数,scanf() 是输入函数,但是它们的工作原理几乎相同。两个函数都使用格式字符串和参数列表。我们先介绍 printf(),再介绍 scanf()。

一、printf() 函数

请求 printf() 函数打印数据的指令要与待打印数据的类型相匹配。例如,打印整数时使用 %d,打印字符时使用 %c。这些符号被称为转换说明(conversion specification),它们指定了如何把数据转换成可显示的形式。我们先列出 ANSI C 标准为 printf() 提供的转换说明,然后再示范如何使用一些较常见的转换说明。表 3 列出了一些转换说明和各自对应的输出类型。

表 3 转换说明及其打印的输出结果

转换说明 输出
%a 浮点数、十六进制数和p记数法(C99/C11)
%A 浮点数、十六进制数和p记数法(C99/C11)
%c 单个字符
%d 有符号十进制整数
%e 浮点数,e记数法
%E 浮点数,e记数法
%f 浮点数,十进制记数法
%g 根据值的不同,自动选择%f%e%e格式用于指数小于-4或者大于或等于精度时
%G 根据值的不同,自动选择%f%E%E格式用于指数小于-4或者大于或等于精度时
%i 有符号十进制整数(与%d相同)
%o 无符号八进制整数
%p 指针
%s 字符串
%u 无符号十进制整数
%x 无符号十六进制整数,使用十六进制数0f
%X 无符号十六进制整数,使用十六进制数0F
%% 打印一个百分号

二、使用 printf()

程序清单 6 的程序中使用了一些转换说明。

程序清单 6 printout.c 程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* printout.c -- 使用转换说明 */
#include <stdio.h>
#define PI 3.141593
int main(void)
{
     int number = 7;
     float pies = 12.75;
     int cost = 7800;

     printf("The %d contestants ate %f berry pies.\n", number,
               pies);
     printf("The value of pi is %f.\n", PI);
     printf("Farewell! thou art too dear for my possessing,\n");
     printf("%c%d\n", '$', 2 * cost);

     return 0;
}

该程序的输出如下:

The 7 contestants ate 12.750000 berry pies.
The value of pi is 3.141593.
Farewell! thou art too dear for my possessing,
$15600

这是 printf() 函数的格式:

1
printf( 格式字符串, 待打印项1, 待打印项2,...);

待打印项 1、待打印项 2 等都是要打印的项。它们可以是变量、常量,甚至是在打印之前先要计算的表达式。格式字符串应包含每个待打印项对应的转换说明。例如,考虑下面的语句:

1
printf("The %d contestants ate %f berry pies.\n", number,pies);

格式字符串是双引号括起来的内容。上面语句的格式字符串包含了两个待打印项 number 和 pies 对应的两个转换说明。图 6 演示了 printf() 语句的另一个例子。

printf()的参数

图 6 printf()的参数

下面是程序清单 6 中的另一行:

1
printf("The value of pi is %f.\n", PI);

该语句中,待打印项列表只有一个项——符号常量 PI。

如图 7 所示,格式字符串包含两种形式不同的信息:

  • 实际要打印的字符;
  • 转换说明。
剖析格式字符串

图 7 剖析格式字符串

警告

格式字符串中的转换说明一定要与后面的每个项相匹配,若忘记这个基本要求会导致严重的后果。千万别写成下面这样:

1
printf("The score was Squids %d, Slugs %d.\n", score1);

这里,第 2 个 %d 没有对应任何项。系统不同,导致的结果也不同。不过,出现这种问题最好的状况是得到无意义的值。

如果只打印短语或句子,就不需要使用任何转换说明。如果只打印数据,也不用加入说明文字。程序清单 6 中的最后两个 printf() 语句都没问题:

1
2
printf("Farewell! thou art too dear for my possessing,\n");
printf("%c%d\n", '$', 2 * cost);

注意第 2 条语句,待打印列表的第 1 个项是一个字符常量,不是变量;第 2 个项是一个乘法表达式。这说明 printf() 使用的是值,无论是变量、常量还是表达式的值。

由于 printf() 函数使用%符号来标识转换说明,因此打印 % 符号就成了个问题。如果单独使用一个 % 符号,编译器会认为漏掉了一个转换字符。解决方法很简单,使用两个 % 符号就行了:

1
2
pc = 2*6;
printf("Only %d%% of Sally's gribbles were edible.\n", pc);

下面是输出结果:

Only 12% of Sally's gribbles were edible.

三、printf() 的转换说明修饰符

% 和转换字符之间插入修饰符可修饰基本的转换说明。表 4 和表 5 列出可作为修饰符的合法字符。如果要插入多个字符,其书写顺序应该与表 4 中列出的顺序相同。不是所有的组合都可行。表中有些字符是 C99 新增的,如果编译器不支持 C99,则可能不支持表中的所有项。

修饰符 含义
标记 表4.5描述了5种标记(-+、空格、#0),可以不使用标记或使用多个标记
示例:"%-10d"
数字 最小字段宽度
如果该字段不能容纳待打印的数字或字符串,系统会使用更宽的字段
示例:"%4d"
.数字 精度
对于%e%E%f转换,表示小数点右边数字的位数
对于%g%G转换,表示有效数字最大位数
对于%s转换,表示待打印字符的最大数量
对于整型转换,表示待打印数字的最小位数
如有必要,使用前导0来达到这个位数
只使用.表示其后跟随一个0,所以%.f%.0f相同
示例:"%5.2f"打印一个浮点数,字段宽度为5字符,其中小数点后有两位数字
h 和整型转换说明一起使用,表示short intunsigned short int类型的值
示例:"%hu""%hx""%6.4hd"
hh 和整型转换说明一起使用,表示signed charunsigned char类型的值
示例:"%hhu""%hhx""%6.4hhd"
j 和整型转换说明一起使用,表示intmax_tuintmax_t类型的值。这些类型定义在stdint.h
示例:"%jd""%8jx"
l 和整型转换说明一起使用,表示long intunsigned long int类型的值
示例:"%ld""%8lu"
ll 和整型转换说明一起使用,表示long long intunsigned long long int类型的值(C99)
示例:"%lld""%8llu"
L 和浮点转换说明一起使用,表示long double类型的值
示例:"%Lf""%10.4Le"
t 和整型转换说明一起使用,表示ptrdiff_t类型的值。ptrdiff_t是两个指针差值的类型(C99)
示例:"%td""%12ti"
z 和整型转换说明一起使用,表示size_t类型的值。size_tsizeof返回的类型(C99)
示例:"%zd""%12zd"

注意 类型可移植性

sizeof 运算符以字节为单位返回类型或值的大小。这应该是某种形式的整数,但是标准只规定了该值是无符号整数。在不同的实现中,它可以是 unsigned int、unsigned long 甚至是 unsigned long long。因此,如果要用 printf() 函数显示 sizeof 表达式,根据不同系统,可能使用 %u%lu%llu。这意味着要查找你当前系统的用法,如果把程序移植到不同的系统还要进行修改。鉴于此,C 提供了可移植性更好的类型。首先,stddef.h 头文件(在包含 stdio.h 头文件时已包含其中)把 size_t 定义成系统使用 sizeof 返回的类型,这被称为底层类型(underlying type)。其次,printf() 使用 z 修饰符表示打印相应的类型。同样,C 还定义了 ptrdiff_t 类型和 t 修饰符来表示系统使用的两个地址差值的底层有符号整数类型。

注意 float参数的转换

对于浮点类型,有用于 double 和 long double 类型的转换说明,却没有 float 类型的转换说明。这是因为在 K&R C 中,表达式或参数中的 float 类型值会被自动转换成 double 类型。一般而言, ANSI C 不会把 float 自动转换成 double 。然而,有大量的现有程序都假设 float 类型的参数被自动转换成 double 类型,为了保护这些程序, printf() 函数中所有 float 类型的参数(对未使用显式原型的所有 C 函数都有效)仍自动转换成 double 类型。因此,无论是 K&R C 还是 ANSI C ,都没有显示 float 类型值专用的转换说明。

表 5 printf() 中的标记

标记 含义
- 待打印项左对齐。即,从字段的左侧开始打印该项
示例:"%-20s"
+ 有符号值若为正,则在值前面显示加号;若为负,则在值前面显示减号
示例:"%+6.2f"
空格 有符号值若为正,则在值前面显示前导空格(不显示任何符号);若为负,则在值前面显示减号+标记并覆盖空格
示例:"%6.2f"
# 把结果转换为另一种形式。如果是%o格式,则以0开始;如果是%x%X格式,则以0x0X开始;对于所有的浮点格式,#保证了即使后面没有任何数字,也打印一个小数点字符。对于%g%G格式,#防止结果后面的0被删除
示例:"%#o""%#8.0f""%+#10.3e"
0 对于数值格式,用前导0代替空格填充字段宽度。对于整数格式,如果出现-标记或指定精度,则忽略该标记
示例:"%010d""%08.3f"

3.1 使用修饰符和标记的示例

接下来,用程序示例演示如何使用这些修饰符和标记。先来看看字段宽度在打印整数时的效果。考虑程序清单 7 中的程序。

程序清单 7 width.c 程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* width.c -- 字段宽度 */
#include <stdio.h>
#define PAGES 959
int main(void)
{
     printf("*%d*\n", PAGES);
     printf("*%2d*\n", PAGES);
     printf("*%10d*\n", PAGES);
     printf("*%-10d*\n", PAGES);

     return 0;
}

程序清单 7 通过 4 种不同的转换说明把相同的值打印了 4 次。程序中使用星号(*)标出每个字段的开始和结束。其输出结果如下所示:

*959*
*959*
*       959*
*959       *

第 1 个转换说明 %d 不带任何修饰符,其对应的输出结果与带整数字段宽度的转换说明的输出结果相同。在默认情况下,没有任何修饰符的转换说明,就是这样的打印结果。第 2 个转换说明是 %2d,其对应的输出结果应该是 2 字段宽度。因为待打印的整数有 3 位数字,所以字段宽度自动扩大以符合整数的长度。第 3 个转换说明是 %10d,其对应的输出结果有 10 个空格宽度,实际上在两个星号之间有 7 个空格和 3 位数字,并且数字位于字段的右侧。最后一个转换说明是 %-10d,其对应的输出结果同样是 10 个空格宽度,- 标记说明打印的数字位于字段的左侧。熟悉它们的用法后,我们就能很好地控制输出格式。试着改变 PAGES 的值,看看编译器如何打印不同位数的数字。

接下来看看浮点型格式。请输入、编译并运行程序清单 8 中的程序。

程序清单 8 floats.c 程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// floats.c -- 一些浮点型修饰符的组合
#include <stdio.h>

int main(void)
{
     const double RENT = 3852.99;  // const变量

     printf("*%f*\n", RENT);
     printf("*%e*\n", RENT);
     printf("*%4.2f*\n", RENT);
     printf("*%3.1f*\n", RENT);
     printf("*%10.3f*\n", RENT);
     printf("*%10.3E*\n", RENT);
     printf("*%+4.2f*\n", RENT);
     printf("*%010.2f*\n", RENT);

     return 0;
}

该程序中使用了 const 关键字,限定变量为只读。该程序的输出如下:

*3852.990000*
*3.852990e+03*
*3852.99*
*3853.0*
*  3852.990*
* 3.853E+03*
*+3852.99*
*0003852.99*

本例的第 1 个转换说明是 %f。在这种情况下,字段宽度和小数点后面的位数均为系统默认设置,即字段宽度是容纳待打印数字所需的位数和小数点后打印 6 位数字。

第 2 个转换说明是 %e。默认情况下,编译器在小数点的左侧打印 1 个数字,在小数点的右侧打印 6 个数字。这样打印的数字太多!解决方案是指定小数点右侧显示的位数,程序中接下来的 4 个例子就是这样做的。请注意,第 4 个和第 6 个例子对输出结果进行了四舍五入。另外,第 6 个例子用 E 代替了 e。

第 7 个转换说明中包含了 + 标记,这使得打印的值前面多了一个代数符号(+)。0 标记使得打印的值前面以 0 填充以满足字段要求。注意,转换说明 %010.2f 的第 1 个 0 是标记,句点(.)之前、标记之后的数字(本例为 10)是指定的字段宽度。

尝试修改 RENT 的值,看看编译器如何打印不同大小的值。程序清单 9 演示了其他组合。

程序清单 9 flags.c 程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* flags.c -- 演示一些格式标记 */
#include <stdio.h>
int main(void)
{
     printf("%x %X %#x\n", 31, 31, 31);
     printf("**%d**% d**% d**\n", 42, 42, -42);
     printf("**%5d**%5.3d**%05d**%05.3d**\n", 6, 6, 6, 6);

     return 0;
}

该程序的输出如下:

1f 1F 0x1f
**42** 42**-42**
**    6**  006**00006**  006**

第 1 行输出中,1f 是十六进制数,等于十进制数 31。第 1 行 printf() 语句中,根据 %x 打印出 1f,%X 打印出 1F,%#x 打印出 0x1f

第 2 行输出演示了如何在转换说明中用空格在输出的正值前面生成前导空格,负值前面不产生前导空格。这样的输出结果比较美观,因为打印出来的正值和负值在相同字段宽度下的有效数字位数相同。

第 3 行输出演示了如何在整型格式中使用精度(%5.3d)生成足够的前导 0 以满足最小位数的要求(本例是 3)。然而,使用 0 标记会使得编译器用前导 0 填充满整个字段宽度。最后,如果 0 标记和精度一起出现,0 标记会被忽略。

下面来看看字符串格式的示例。考虑程序清单 10 中的程序。

程序清单 10 stringf.c 程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* stringf.c -- 字符串格式 */
#include <stdio.h>
#define BLURB "Authentic imitation!"
int main(void)
{
     printf("[%2s]\n", BLURB);
     printf("[%24s]\n", BLURB);
     printf("[%24.5s]\n", BLURB);
     printf("[%-24.5s]\n", BLURB);

     return 0;
}

该程序的输出如下:

[Authentic imitation!]
[     Authentic imitation!]
[                    Authe]
[Authe                    ]

注意,虽然第 1 个转换说明是 %2s,但是字段被扩大为可容纳字符串中的所有字符。还需注意,精度限制了待打印字符的个数。.5 告诉 printf() 只打印 5 个字符。另外,- 标记使得文本左对齐输出。

3.2 学以致用

学习完以上几个示例,试试如何用一个语句打印以下格式的内容:

The NAME family just may be $XXX.XX dollars richer!

这里,NAME 和 XXX.XX 代表程序中变量(如 name[40] 和 cash)的值。可参考以下代码:

1
printf("The %s family just may be $%.2f dollars richer!\n",name,cash);

四、转换说明的意义

下面深入探讨一下转换说明的意义。转换说明把以二进制格式存储在计算机中的值转换成一系列字符(字符串)以便于显示。例如,数字 76 在计算机内部的存储格式是二进制数 01001100。%d 转换说明将其转换成字符 7 和 6,并显示为 76;%x 转换说明把相同的值(01001100)转换成十六进制记数法 4c;%c 转换说明把 01001100 转换成字符 L。

转换(conversion)可能会误导读者认为原始值被替换成转换后的值。实际上,转换说明是翻译说明,%d 的意思是“把给定的值翻译成十进制整数文本并打印出来”。

4.1 转换不匹配

前面强调过,转换说明应该与待打印值的类型相匹配。通常都有多种选择。例如,如果要打印一个 int 类型的值,可以使用 %d%x%o。这些转换说明都可用于打印 int 类型的值,其区别在于它们分别表示一个值的形式不同。类似地,打印 double 类型的值时,可使用 %f%e%g

转换说明与待打印值的类型不匹配会怎样?匹配非常重要,一定要牢记于心。程序清单 11 演示了一些不匹配的整型转换示例。

程序清单 11 intconv.c 程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* intconv.c -- 一些不匹配的整型转换 */
#include <stdio.h>
#define PAGES 336
#define WORDS 65618
int main(void)
{
     short num = PAGES;
     short mnum = -PAGES;

     printf("num as short and unsigned short:  %hd %hu\n", num,num);
     printf("-num as short and unsigned short: %hd %hu\n", mnum,mnum);
     printf("num as int and char: %d %c\n", num, num);
     printf("WORDS as int, short, and char: %d %hd %c\n",WORDS,WORDS, WORDS);

     return 0;
}

在我们的系统中,该程序的输出如下:

num as short and unsigned short: 336 336
-num as short and unsigned short: -336 65200
num as int and char: 336 P
WORDS as int, short, and char: 65618 82 R

请看输出的第 1 行,num 变量对应的转换说明 %hd%hu 输出的结果都是 336。这没有任何问题。然而,第 2 行 mnum 变量对应的转换说明 %u(无符号)输出的结果却为 65200,并非期望的 336。这是由于有符号 short int 类型的值在我们的参考系统中的表示方式所致。首先,short int 的大小是 2 字节;其次,系统使用二进制补码来表示有符号整数。这种方法,数字 0~32767 代表它们本身,而数字 32768~65535 则表示负数。其中,65535 表示 -1,65534 表示 -2,以此类推。因此,-336 表示为 65200(即,65536-336)。所以被解释成有符号 int 时,65200 代表 -336;而被解释成无符号 int 时,65200 则代表 65200。一定要谨慎!一个数字可以被解释成两个不同的值。尽管并非所有的系统都使用这种方法来表示负整数,但要注意一点:别期望用 %u 转换说明能把数字和符号分开。

第 3 行演示了如果把一个大于 255 的值转换成字符会发生什么情况。在我们的系统中,short int 是 2 字节,char 是 1 字节。当 printf() 使用 %c 打印 336 时,它只会查看存储 336 的 2 字节中的后 1 字节。这种截断(见图 8)相当于用一个整数除以 256,只保留其余数。在这种情况下,余数是 80,对应的 ASCII 值是字符 P。用专业术语来说,该数字被解释成“以 256 为模”(modulo 256),即该数字除以 256 后取其余数。

把 336 转换成字符

图 8 把 336 转换成字符

最后,我们在该系统中打印比 short int 类型最大整数(32767)更大的整数(65618)。这次,计算机也进行了求模运算。在本系统中,应把数字 65618 存储为 4 字节的 int 类型值。用 %hd 转换说明打印时,printf() 只使用最后 2 个字节。这相当于 65618 除以 65536 的余数。这里,余数是 82。鉴于负数的存储方法,如果余数在 32767~65536 范围内会被打印成负数。对于整数大小不同的系统,相应的处理行为类似,但是产生的值可能不同。

混淆整型和浮点型,结果更奇怪。考虑程序清单 12。

程序清单 12 floatcnv.c 程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* floatcnv.c -- 不匹配的浮点型转换 */
#include <stdio.h>
int main(void)
{
     float n1 = 3.0;
     double n2 = 3.0;
     long n3 = 2000000000;
     long n4 = 1234567890;

     printf("%.1e %.1e %.1e %.1e\n", n1, n2, n3, n4);
     printf("%ld %ld\n", n3, n4);
     printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);

     return 0;
}

在我们的系统中,该程序的输出如下:

3.0e+00 3.0e+00 3.1e+46 1.7e+266
2000000000 1234567890
0 1074266112 0 1074266112

第 1 行输出显示,%e 转换说明没有把整数转换成浮点数。考虑一下,如果使用 %e 转换说明打印 n3(long 类型)会发生什么情况。首先,%e 转换说明让 printf() 函数认为待打印的值是 double 类型(本系统中 double 为 8 字节)。当 printf() 查看 n3(本系统中是 4 字节的值)时,除了查看 n3 的 4 字节外,还会查看查看 n3 相邻的 4 字节,共 8 字节单元。接着,它将 8 字节单元中的位组合解释成浮点数(如,把一部分位组合解释成指数)。因此,即使 n3 的位数正确,根据 %e 转换说明和 %ld 转换说明解释出来的值也不同。最终得到的结果是无意义的值。

第 1 行也说明了前面提到的内容:float 类型的值作为 printf() 参数时会被转换成 double 类型。在本系统中,float 是 4 字节,但是为了 printf() 能正确地显示该值,n1 被扩成 8 字节。

第 2 行输出显示,只要使用正确的转换说明,printf() 就可以打印 n3 和 n4。

第 3 行输出显示,如果 printf() 语句有其他不匹配的地方,即使用对了转换说明也会生成虚假的结果。用 %ld 转换说明打印浮点数会失败,但是在这里,用 %ld 打印 long 类型的数竟然也失败了!问题出在 C 如何把信息传递给函数。具体情况因编译器实现而异。“参数传递”框中针对一个有代表性的系统进行了讨论。

参数传递

参数传递机制因实现而异。下面以我们的系统为例,分析参数传递的原理。函数调用如下:

1
printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);

该调用告诉计算机把变量 n1、n2、n3 和 n4 的值传递给程序。这是一种常见的参数传递方式。程序把传入的值放入被称为栈(stack)的内存区域。计算机根据变量类型(不是根据转换说明)把这些值放入栈中。因此,n1 被存储在栈中,占 8 字节(float 类型被转换成 double 类型)。同样,n2 也在栈中占 8 字节,而 n3 和 n4 在栈中分别占 4 字节。然后,控制转到 printf() 函数。该函数根据转换说明(不是根据变量类型)从栈中读取值。%ld 转换说明表明 printf() 应该读取 4 字节,所以 printf() 读取栈中的前 4 字节作为第 1 个值。这是 n1 的前半部分,将被解释成一个 long 类型的整数。根据下一个 %ld 转换说明,printf() 再读取 4 字节,这是 n1 的后半部分,将被解释成第 2 个 long 类型的整数(见图 9)。类似地,根据第 3 个和第 4 个 %ld,printf() 读取 n2 的前半部分和后半部分,并解释成两个 long 类型的整数。因此,对于 n3 和 n4,虽然用对了转换说明,但 printf() 还是读错了字节。

1
2
3
4
5
float n1;    /* 作为double类型传递 */
double n2;
long n3, n4;
...
printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);
传递参数

图 9 传递参数

4.2 printf() 的返回值

大部分 C 函数都有一个返回值,这是函数计算并返回给主调程序(calling program)的值。例如,C 库包含一个 sqrt() 函数,接受一个数作为参数,并返回该数的平方根。可以把返回值赋给变量,也可以用于计算,还可以作为参数传递。总之,可以把返回值像其他值一样使用。printf() 函数也有一个返回值,它返回打印字符的个数。如果有输出错误,printf() 则返回一个负值(printf() 的旧版本会返回不同的值)。

printf() 的返回值是其打印输出功能的附带用途,通常很少用到,但在检查输出错误时可能会用到(如,在写入文件时很常用)。如果一张已满的 CD 或 DVD 拒绝写入,程序应该采取相应的行动,例如终端蜂鸣 30 秒。不过,要实现这种情况必须先了解 if 语句。程序清单 13 演示了如何确定函数的返回值。

程序清单 13 prntval.c 程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* prntval.c -- printf()的返回值 */
#include <stdio.h>
int main(void)
{
     int bph2o = 212;
     int rv;

     rv = printf("%d F is water's boiling point.\n", bph2o);
     printf("The printf() function printed %d characters.\n",
               rv);
     return 0;
}

该程序的输出如下:

212 F is water's boiling point.
The printf() function printed 32 characters.

首先,程序用 rv = printf(...); 的形式把 printf() 的返回值赋给 rv。因此,该语句执行了两项任务:打印信息和给变量赋值。其次,注意计算针对所有字符数,包括空格和不可见的换行符(\n)。

4.3 打印较长的字符串

有时,printf() 语句太长,在屏幕上不方便阅读。如果空白(空格、制表符、换行符)仅用于分隔不同的部分,C 编译器会忽略它们。因此,一条语句可以写成多行,只需在不同部分之间输入空白即可。例如,程序清单 13 中的一条 printf() 语句:

1
2
printf("The printf() function printed %d characters.\n",
          rv);

该语句在逗号和 rv 之间断行。为了让读者知道该行未完,示例缩进了 rv。C 编译器会忽略多余的空白。

但是,不能在双引号括起来的字符串中间断行。如果这样写:

1
2
printf("The printf() function printed %d
           characters.\n", rv);

C 编译器会报错:字符串常量中有非法字符。在字符串中,可以使用 \n 来表示换行字符,但是不能通过按下 Enter(或 Return)键产生实际的换行符。

给字符串断行有 3 种方法,如程序清单 14 所示。

程序清单 14 longstrg.c 程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* longstrg.c ––打印较长的字符串 */
#include <stdio.h>
int main(void)
{
     printf("Here's one way to print a ");
     printf("long string.\n");
     printf("Here's another way to print a \
long string.\n");
     printf("Here's the newest way to print a "
               "long string.\n");      /* ANSI C */

     return 0;
}

该程序的输出如下:

Here's one way to print a long string.
Here's another way to print a long string.
Here's the newest way to print a long string.

方法 1:使用多个 printf() 语句。因为第 1 个字符串没有以 \n 字符结束,所以第 2 个字符串紧跟第 1 个字符串末尾输出。

方法2:用反斜杠(\)和 Enter(或 Return)键组合来断行。这使得光标移至下一行,而且字符串中不会包含换行符。其效果是在下一行继续输出。但是,下一行代码必须和程序清单中的代码一样从最左边开始。如果缩进该行,比如缩进 5 个空格,那么这 5 个空格就会成为字符串的一部分。

方法3:ANSI C 引入的字符串连接。在两个用双引号括起来的字符串之间用空白隔开,C 编译器会把多个字符串看作是一个字符串。因此,以下3种形式是等效的:

1
2
3
4
printf("Hello, young lovers, wherever you are.");
printf("Hello, young "        "lovers" ", wherever you are.");
printf("Hello, young lovers"
        ", wherever you are.");

上述方法中,要记得在字符串中包含所需的空格。如,"young""lovers" 会成为 "younglovers",而 "young " "lovers" 才是 "younglovers"

五、使用 scanf()

刚学完输出,接下来我们转至输入——学习 scanf() 函数。C 库包含了多个输入函数,scanf() 是最通用的一个,因为它可以读取不同格式的数据。当然,从键盘输入的都是文本,因为键盘只能生成文本字符:字母、数字和标点符号。如果要输入整数 2014,就要键入字符 2、0、1、4。如果要将其存储为数值而不是字符串,程序就必须把字符依次转换成数值,这就是 scanf() 要做的。scanf() 把输入的字符串转换成整数、浮点数、字符或字符串,而 printf() 正好与它相反,把整数、浮点数、字符和字符串转换成显示在屏幕上的文本。

scanf() 和printf() 类似,也使用格式字符串和参数列表。scanf() 中的格式字符串表明字符输入流的目标数据类型。两个函数主要的区别在参数列表中。printf() 函数使用变量、常量和表达式,而 scanf() 函数使用指向变量的指针。这里,读者不必了解如何使用指针,只需记住以下两条简单的规则:

  • 如果用 scanf() 读取基本变量类型的值,在变量名前加上一个 &
  • 如果用 scanf() 把字符串读入字符数组中,不要使用 &

程序清单 15 中的小程序演示了这两条规则。

程序清单 15 input.c 程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// input.c -- 何时使用&
#include <stdio.h>
int main(void)
{
     int age;             // 变量
     float assets;        // 变量
     char pet[30];        // 字符数组,用于存储字符串

     printf("Enter your age, assets, and favorite pet.\n");
     scanf("%d %f", &age, &assets);   // 这里要使用&
     scanf("%s", pet);                // 字符数组不使用&
     printf("%d $%.2f %s\n", age, assets, pet);

     return 0;
}

下面是该程序与用户交互的示例:

Enter your age, assets, and favorite pet.
38
92360.88 llama
38 $92360.88 llama

scanf() 函数使用空白(换行符、制表符和空格)把输入分成多个字段。在依次把转换说明和字段匹配时跳过空白。注意,上面示例的输入项(粗体部分是用户的输入)分成了两行。只要在每个输入项之间输入至少一个换行符、空格或制表符即可,可以在一行或多行输入:

Enter your age, assets, and favorite pet.
  42

     2121.45

     guppy
42 $2121.45 guppy

唯一例外的是 %c 转换说明。根据 %c,scanf() 会读取每个字符,包括空白。我们稍后详述这部分。

scanf() 函数所用的转换说明与 printf() 函数几乎相同。主要的区别是,对于 float 类型和 double 类型,printf() 都使用 %f%e%E%g%G 转换说明。而 scanf() 只把它们用于 float 类型,对于 double 类型要使用l修饰符。表 6 列出了 C99 标准中常用的转换说明。

表 6 ANSI C 中 scanf() 的转换说明

转换说明 含义
%c 把输入解释成字符
%d 把输入解释成有符号十进制整数
%e%f%g%a 把输入解释成浮点数(C99标准新增了%a
%E%F%G%A 把输入解释成浮点数(C99标准新增了%A
%i 把输入解释成有符号十进制整数
%o 把输入解释成有符号八进制整数
%p 把输入解释成指针(地址)
%s 把输入解释成字符串。从第1个非空白字符开始,到下一个空白字符之前的所有字符都是输入
%u 把输入解释成无符号十进制整数
%x%X 把输入解释成有符号十六进制整数

可以在表 6 所列的转换说明中(百分号和转换字符之间)使用修饰符。如果要使用多个修饰符,必须按表 7 所列的顺序书写。

表 7 scanf() 转换说明中的修饰符

转换说明 含义
* 抑制赋值(详见后面解释)
示例:"%*d"
数字 最大字段宽度。输入达到最大字段宽度处,或第1次遇到空白字符时停止
示例:"%10s"
hh 把整数作为signed charunsigned char类型读取
示例:"%hhd""%hhu"
ll 把整数作为long longunsigned long long类型读取(C99
示例:"%lld""%llu"
hlL "%hd""%hi"表明把对应的值存储为short int类型
"%ho""%hx""%hu"表明把对应的值存储为unsigned short int类型
"%ld""%li"表明把对应的值存储为long类型
"%lo""%lx""%lu"表明把对应的值存储为unsigned long类型
"%le""%lf""%lg"表明把对应的值存储为double类型
efg前面使用L而不是l,表明把对应的值被存储为long double类型。如果没有修饰符,diox表明对应的值被存储为int类型,fg表明把对应的值存储为float类型
j 在整型转换说明后面时,表明使用intmax_tuintmax_t类型(C99)
示例:"%jd""%ju"
z 在整型转换说明后面时,表明使用sizeof的返回类型(C99)
示例:"%zd""%zo"
t 在整型转换说明后面时,表明使用表示两个指针差值的类型(C99)
示例:"%td""%tx"

如你所见,使用转换说明比较复杂,而且这些表中还省略了一些特性。省略的主要特性是,从高度格式化源中读取选定数据,如穿孔卡或其他数据记录。因为在本文中,scanf() 主要作为与程序交互的便利工具,所以我们不在文中讨论更复杂的特性。

5.1 从 scanf() 角度看输入

接下来,我们更详细地研究 scanf() 怎样读取输入。假设 scanf() 根据一个 %d 转换说明读取一个整数。scanf() 函数每次读取一个字符,跳过所有的空白字符,直至遇到第 1 个非空白字符才开始读取。因为要读取整数,所以 scanf() 希望发现一个数字字符或者一个符号(+ 或 -)。如果找到一个数字或符号,它便保存该字符,并读取下一个字符。如果下一个字符是数字,它便保存该数字并读取下一个字符。scanf() 不断地读取和保存字符,直至遇到非数字字符。如果遇到一个非数字字符,它便认为读到了整数的末尾。然后,scanf() 把非数字字符放回输入。这意味着程序在下一次读取输入时,首先读到的是上一次读取丢弃的非数字字符。最后,scanf() 计算已读取数字(可能还有符号)相应的数值,并将计算后的值放入指定的变量中。

如果使用字段宽度,scanf() 会在字段结尾或第 1 个空白字符处停止读取(满足两个条件之一便停止)。

如果第 1 个非空白字符是 A 而不是数字,会发生什么情况?scanf() 将停在那里,并把 A 放回输入中,不会把值赋给指定变量。程序在下一次读取输入时,首先读到的字符是 A。如果程序只使用 %d 转换说明,scanf() 就一直无法越过 A 读下一个字符。另外,如果使用带多个转换说明的 scanf(),C 规定在第 1 个出错处停止读取输入。

用其他数值匹配的转换说明读取输入和用 %d 的情况相同。区别在于 scanf() 会把更多字符识别成数字的一部分。例如,%x 转换说明要求 scanf() 识别十六进制数 a~f 和 A~F。浮点转换说明要求 scanf() 识别小数点、e 记数法(指数记数法)和新增的 p 记数法(十六进制指数记数法)。

如果使用 %s 转换说明,scanf() 会读取除空白以外的所有字符。scanf() 跳过空白开始读取第 1 个非空白字符,并保存非空白字符直到再次遇到空白。这意味着 scanf() 根据 %s 转换说明读取一个单词,即不包含空白字符的字符串。如果使用字段宽度,scanf() 在字段末尾或第 1 个空白字符处停止读取。无法利用字段宽度让只有一个 %s 的 scanf() 读取多个单词。最后要注意一点:当 scanf() 把字符串放进指定数组中时,它会在字符序列的末尾加上 '\0',让数组中的内容成为一个 C 字符串。

实际上,在 C 语言中 scanf() 并不是最常用的输入函数。这里重点介绍它是因为它能读取不同类型的数据。C 语言还有其他的输入函数,如 getchar() 和 fgets()。这两个函数更适合处理一些特殊情况,如读取单个字符或包含空格的字符串。目前,无论程序中需要读取整数、小数、字符还是字符串,都可以使用 scanf() 函数。

5.2 格式字符串中的普通字符

scanf() 函数允许把普通字符放在格式字符串中。除空格字符外的普通字符必须与输入字符串严格匹配。例如,假设在两个转换说明中添加一个逗号:

1
scanf("%d,%d", &n, &m);

scanf() 函数将其解释成:用户将输入一个数字、一个逗号,然后再输入一个数字。也就是说,用户必须像下面这样进行输入两个整数:

88,121

由于格式字符串中,%d 后面紧跟逗号,所以必须在输入 88 后再输入一个逗号。但是,由于 scanf() 会跳过整数前面的空白,所以下面两种输入方式都可以:

88, 121

88,
121

格式字符串中的空白意味着跳过下一个输入项前面的所有空白。例如,对于下面的语句:

1
scanf("%d ,%d", &n, &m);

以下的输入格式都没问题:

88,121
88 ,121
88 , 121

请注意,“所有空白”的概念包括没有空格的特殊情况。

除了 %c,其他转换说明都会自动跳过待输入值前面所有的空白。因此,scanf("%d%d", &n, &m)scanf("%d %d", &n, &m) 的行为相同。对于 %c,在格式字符串中添加一个空格字符会有所不同。例如,如果在格式字符串中把空格放到 %c 的前面,scanf() 便会跳过空格,从第 1 个非空白字符开始读取。也就是说,scanf("%c", &ch) 从输入中的第 1 个字符开始读取,而 scanf(" %c", &ch) 则从第 1 个非空白字符开始读取。

5.3 scanf() 的返回值

scanf() 函数返回成功读取的项数。如果没有读取任何项,且需要读取一个数字而用户却输入一个非数值字符串,scanf() 便返回 0。当 scanf() 检测到“文件结尾”时,会返回 EOF(EOF 是 stdio.h 中定义的特殊值,通常用 #define 指令把 EOF 定义为 -1)。在读者学会 if 语句和 while 语句后,便可使用 scanf() 的返回值来检测和处理不匹配的输入。

六、printf() 和 scanf() 的 * 修饰符

printf() 和 scanf() 都可以使用 * 修饰符来修改转换说明的含义。但是,它们的用法不太一样。首先,我们来看 printf() 的 * 修饰符。

如果你不想预先指定字段宽度,希望通过程序来指定,那么可以用 * 修饰符代替字段宽度。但还是要用一个参数告诉函数,字段宽度应该是多少。也就是说,如果转换说明是 %*d,那么参数列表中应包含 *d 对应的值。这个技巧也可用于浮点值指定精度和字段宽度。程序清单 16 演示了相关用法。

程序清单 16 varwid.c 程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* varwid.c -- 使用变宽输出字段 */
#include <stdio.h>
int main(void)
{
     unsigned width, precision;
     int number = 256;
     double weight = 242.5;

     printf("Enter a field width:\n");
     scanf("%d", &width);
     printf("The number is :%*d:\n", width, number);
     printf("Now enter a width and a precision:\n");
     scanf("%d %d", &width, &precision);
     printf("Weight = %*.*f\n", width, precision, weight);
     printf("Done!\n");

     return 0;
}

变量 width 提供字段宽度,number 是待打印的数字。因为转换说明中 *d 的前面,所以在 printf() 的参数列表中,width 在 number 的前面。同样,width 和 precision 提供打印 weight 的格式化信息。下面是一个运行示例:

Enter a field width:
6
The number is :   256:
Now enter a width and a precision:
8 3
Weight =  242.500
Done!

这里,用户首先输入 6,因此 6 是程序使用的字段宽度。类似地,接下来用户输入 8 和 3,说明字段宽度是 8,小数点后面显示 3 位数字。一般而言,程序应根据 weight 的值来决定这些变量的值。

scanf() 中 * 的用法与此不同。把 * 放在%和转换字符之间时,会使得 scanf() 跳过相应的输入项。程序清单 17 就是一个例子。

程序清单 17 skip2.c 程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* skiptwo.c -- 跳过输入中的前两个整数 */
#include <stdio.h>
int main(void)
{
     int n;

     printf("Please enter three integers:\n");
     scanf("%*d %*d %d", &n);
     printf("The last integer was %d\n", n);

     return 0;
}

程序清单 17 中的 scanf() 指示:跳过两个整数,把第 3 个整数拷贝给 n。下面是一个运行示例:

Please enter three integers:
2013 2014 2015
The last integer was 2015

在程序需要读取文件中特定列的内容时,这项跳过功能很有用。

七、printf() 的用法提示

想把数据打印成列,指定固定字段宽度很有用。因为默认的字段宽度是待打印数字的宽度,如果同一列中打印的数字位数不同,那么下面的语句:

1
printf("%d %d %d\n", val1, val2, val3);

打印出来的数字可能参差不齐。例如,假设执行 3 次 printf() 语句,用户输入不同的变量,其输出可能是这样:

12 234 1222
4 5 23
22334 2322 10001

使用足够大的固定字段宽度可以让输出整齐美观。例如,若使用下面的语句:

1
printf("%9d %9d %9d\n", val1, val2, val3);

上面的输出将变成:

   12      234      1222
    4        5        23
22334     2322     10001

在两个转换说明中间插入一个空白字符,可以确保即使一个数字溢出了自己的字段,下一个数字也不会紧跟该数字一起输出(这样两个数字看起来像是一个数字)。这是因为格式字符串中的普通字符(包括空格)会被打印出来。

另一方面,如果要在文字中嵌入一个数字,通常指定一个小于或等于该数字宽度的字段会比较方便。这样,输出数字的宽度正合适,没有不必要的空白。例如,下面的语句:

1
printf("Count Beppo ran %.2f miles in 3 hours.\n", distance);

其输出如下:

Count Beppo ran 10.22 miles in 3 hours.

如果把转换说明改为 %10.2f,则输出如下:

Count Beppo ran      10.22 miles in 3 hours.

本地化设置

美国和世界上的许多地区都使用一个点来分隔十进制值的整数部分和小数部分,如 3.14159。然而,许多其他地区用逗号来分隔,如 3,14159。读者可能注意到了,printf() 和 scanf() 都没有提供逗号的转换说明。C 语言考虑了这种情况。因此 C 程序可以选择特定的本地化设置。例如,如果指定了荷兰语言环境,printf() 和 scanf() 在显示和读取浮点值时会使用本地惯例(在这种情况下,用逗号代替点分隔浮点值的整数部分和小数部分)。另外,一旦指定了环境,便可在代码的数字中使用逗号:

1
double pi = 3,14159; // 荷兰本地化设置

C 标准有两个本地化设置:“C"和”"(空字符串)。默认情况下,程序使用"C"本地化设置,基本上符合美国的用法习惯。而"“本地化设置可以替换当前系统中使用的本地语言环境。原则上,这与"C"本地化设置相同。事实上,大部分操作系统(如 UNIX、Linux 和 Windows)都提供本地化设置选项列表,只不过它们提供的列表可能不同。

(完)

comments powered by Disqus