6章 格式化输出/输入

使用计算机程序解决问题时,需要由用户输入数据,通过程序进行运算后,再输出结果。由此过程可以看到,程序中输入/输出占有相当重要的地位。在 C 语言中,所有的数据输入/输出都是由库函数完成的,因此都是函数语句。在前面各章的实例中,曾使用过printf函数和scanf函数,进行数据的输出和输入,本章将详细介绍这两个函数的使用。

6.1 格式化输出——printf函数

C程序运算的结果保存在内存中,必须将其输出到指定设备(如显示器、打印机等),才能让用户了解程序的运行结果。显示器是计算机的标配设备,将程序运行结果输出到显示器是最常用的方法。使用库函数printf,即可方便地将数据输出到显示器上。

6.1.1 printf函数的格式

在本书前面各章中,已多次用到printf函数,它的作用是向显示器(终端)输出多个任意类型的数据。printf函数的语法格式如下:

        printf(格式控制,输出列表)

该函数包括两部分参数,其作用分别如下:

格式控制:是用双引号括起来的字符串,在该字符串中,可以只包含普通字符,这时printf函数将其原样输出到屏幕上。在字符串中,还可包含“%”开头的格式控制字符,如“%d”,“%f”等。它的作用是将输出的数据转换为指定的格式输出。格式说明总是由“%”字符开始的,printf 函数格式控制字符很多,在后面将详细介绍。

输出列表:是需要输出到屏幕的数据,可以是常量、变量或表达式,若为表达式,将输出表达式的值。

【程序6-1】printf函数示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:      int i=8;
        6:      float f=0.618;
        7:
        8:      printf("1.只输出文字信息!\n");
        9:      printf("2.整型变量i=%d\t实型变量f=%f\n",i,f);
        10:     getch();
        11:     return 0;
        12: }

编译执行以上代码,得到如图6-1所示的结果。

图6-1 【程序6-1】执行结果

第8行程序中的printf函数只有一个参数“格式控制字符串”,省略了该函数后面的“输出列表”参数。所以该语句只输出一串文字信息,引号最后的转义符“\n”进行换行,即后面输出的内容将出现在新的一行中。

第9行的printf函数包括了两部分参数,“格式控制字符串”中包括三部分内容:提示文字、控制字符、转义字符。“输出列表”由两个变量组成,用逗号分开。各部分如下所示。

在上面双引号中的字符除了“%d”和“%f”以外,还有非格式说明的普通字符,它们按原样输出。

在“格式控制字符串”中的每个格式字符,都需要在“输出列表”中有一个表达式与之对应。否则,将得到不可预料的后果。例如,以下输出语句可能得到一个无意义的数据:

        printf("%d%d",i);

在以上语句中,printf函数中使用了两个控制字符“%d”,第一个对应变量i,第二个“%d”无对应的变量输出,C 编译器可能会将不可知内存区域的内容输出到此处,得到一个无意义的数字。

6.1.2 printf的格式字符

在 printf 函数的第一个参数中必须包含有以“%”开头的格式字符,才能将输出列表中表达式的值按规定的格式输出。对于输出列表中的同一个值,使用不同的格式字符,将得到不同的输出结果。例如,观察以下程序的运行结果,该程序使用不同的格式字符输出同一个变量的值。

【程序6-2】printf函数不同格式串输出示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:      int i=68;
        6:
        7:      printf("整数%d,输出为字符:%c\n",i,i);
        8:      printf("整数%d,输出为八进制数:%o\n",i,i);
        9:      printf("整数%d,输入为十六进制数:%X\n",i,i);
        10:     getch();
        11:     return 0;
        12: }

编译执行以上代码,将得到如图6-2所示的结果。

图6-2 【程序6-2】执行结果

以上程序通过格式字符%c、%o、%X将整型数输出为字符、八进制数和十六进制数。

在本书第4章中介绍各种数据类型的输出时,曾分别介绍了输出各种数据类型时使用的控制符,本章将这些格式字符汇总,如表6-1所示。

表6-1 格式字符汇总表

6.1.3 修饰符

printf 函数的格式控制字符串用于指定输出格式。格式字符是以%开头的字符串,在“%”后面跟有各种格式字符,以说明输出数据的类型。在“%”和格式字符之间还可以使用一个或多个修饰符,对输出的格式进一步进行控制,如控制对齐方式、输出长度、小数位数等。如“%d”表示按十进制整型输出,“%ld”表示按十进制长整型输出,“%4d”表示整型数至少占4个字符宽度等。

对printf函数的格式字符进行扩展,加入修饰符后的一般形式如下:

          [标志][输出最小宽度][.精度][长度]类型

这些修饰符使用方括号[]括起来,表示在使用时可省略,在程序代码中,不能输入方括号。下面分别介绍修饰符的意义。

1.标志

共有5种标志,可以使用0个或多个标志。各标志的意义如表6-2所示。

表6-2 标志意义

2.输出最小宽度

用十进制整数来表示输出的最少位数。若实际位数多于定义的宽度,则按实际位数输出,若实际位数少于定义的宽度,则补以空格或0(以标志位来控制)。

结合标志编写以下实例程序,验证标志和输出最小宽度的效果。

【程序6-3】printf函数输出最小宽度控制示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:      int i=-78,j=65535;
        6:
        7:      printf("%d%d\n",i,j);
        8:      printf("%4d%4d\n",i,j);
        9:      printf("%-4d%-4d\n",i,j);
        10:     printf("%+4d%+4d\n",i,j);
        11:     printf("%#x%#X\n",j,j);
        12:     printf("%04d%04d\n",i,j);
        13:     printf("% d% d\n",i,j);
        14:     getch();
        15:     return 0;
        16: }

编译执行以上程序,得到如图6-3所示结果。

图6-3 【程序6-3】执行结果

第7行程序只使用格式字符输出整型变量,因两个格式字符是紧靠着的,所以输入的两个数也紧靠在一起,看起来像一个数一样。

第8行程序设置输出每个变量至少占4个字符宽,变量i的值为两位数,加上负号一共占三位,所以左侧空一个字符位(默认是右对齐的)。变量 j 的值有五位数,超过了 4位数的宽度要求,将紧接着前一个输出项,输出全部的五位数。

第9行程序在格式字符中使用“-”修饰符,并设置输出每个变量至少占4位宽度,这时,变量i的值占三位,但其输出内容靠左对齐,右侧留一个空格。变量j的值占五位,超过了最小宽度,紧接着上一个输出项输出。

第10行程序使用修饰符“+”,使输出的正数也显示正号。

第11行程序使用修饰符“#”,使输出的十六进制数加上前缀“0x”或“0X”。

第10行程序使用修饰符“0”,并设置最小宽度为4,变量i的值占三位,将在该数前面填充一个0。变量j的值占五位,将不会填充前置0。

第12行程序使用修饰符空格,变量i为负数,仍然输出负号,变量i为正数,将不显示正号,而显示一个空格。

3.精度

精度修饰符以“.”开头,后跟十进制整数。该修饰符的意义是:如果输出为整数,表示输出的最小位数,若输出数的位数小于该值,将添加前置 0。如果输出的为实数,则表示小数的位数;如果输出的是字符,则表示输出字符的个数;若实际位数大于所定义的精度数,则截去超过的部分。

【程序6-4】printf函数输出精度控制示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:      int j=65535;
        6:      float f=3.1415926;
        7:
        8:      printf("j=%.8d\n",j);
        9:      printf("f=%.2f\n",f);
        10:     printf("f=%07.2f\n",f);
        11:     getch();
        12:     return 0;
        13: }

编译执行以上代码,得到如图6-4所示结果。

图6-4 【程序6-4】执行结果

第8行程序使用精度修饰符控制整型变量j的最少输出宽度为8,而该变量的宽度为5,所以,在前面添加3个前置0。

第9行程序精度为2,只输出变量f的两位小数,其余部分不显示。

第 10 行程序的格式字符使用了标志、宽度、精度等多个修饰符。设置输出宽度为 7位,小数位数为2位,变量f的整数部分只有1位,小数输出2位,小数点占一位,所以,前面添加3个前置0,凑足7位。

4.长度

这里所说的长度,是指输出数据占用内存的长度,其实就是指输出指定类型的数据。在正常情况下,使用“%d”格式字符,输入int类型的数据,若要输出long类型的数据,则需使用“%ld”(字母L的小写,不是数字1),其中的“l”就是长度修饰符。可使用表6-3所示的长度修饰符。

表6-3 长度修饰符

现在大部分都是32位字长计算机系统,int类型和long类型使用相同的字节数,在printf函数中,使用“%d”可输出这两种类型的数据,也可使用“%ld”来输出long类型的数据,使用程序在移植到16位字长的计算机系统中,也可正确使用。

对于short和char类型的数据,使用“%d”也可正常输出。

6.1.4 printf函数实例

前面集中介绍了printf的格式字符,以及各种修饰符的使用。本节按输出数据类型的实例,演示这些格式字符的使用。

1.输出字符

使用格式字符“%c”可将整数类型的数据输出为一个对应的ASCII字符。char类型的取值范围为-128~127,正好对应一个ASCII字符。如果数据为int类型或long类型等整数类型,由于这些数据类型的取值范围超过了ASCII字符集的数量,printf函数采用什么方式将其转换为ASCII字符呢。下面的程序演示具体的转换过程。

【程序6-5】printf函数输出字符示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:      int i=75,j=65355;
        6:      char c='w';
        7:
        8:      printf("整型数:%d,输出为字符:%c\n",i,i);
        9:      printf("整型数:%d,输出为字符:%c\n",j,j);
        10:     printf("字符:%c,输出为整型数:%d\n",c,c);
        11:     getch();
        12:     return 0;
        13: }

编译执行以上代码,得到如图6-5所示的结果。

图6-5 【程序6-5】执行结果

第8行程序将整数75按字符格式输出,得到字符“K”。

第9行程序将整数65355按字符格式输出,也得到字符“K”。

第10行程序将字符“w”按整数格式输出,得到整数119。

在第8行和第9行程序中,针对两个不同的整数75和65355,都输出字符“K”,这是因为使用printf函数的“%c”格式字符时,printf函数将只读取指定值的低8位(1个字节)的内容来进行输出,当数据的占用的字节数超过1个字节时,将只输出该数据最低位1个字节的内容。

将数据 65355 按 int 变量保存在内存中时,计算机会将该数转换为二进制,然后将 8位二进制为一个单位存入内存区域。为了表示方便,将二进制转换为十六进制,每个字节可保存两位十六进制数。65355转换为十六进制数为FF4B,将该数按int类型存储到内存中的示意图如图6-6所示。

图6-6 内存数据示意图

从图6-6可以看出,最低字节保存的值为十六进制的4B,转换为十进制为75,所以输出的字符和第6行程序输出的结果相同。

类似地,将short int、long int和long long int类型的数据使用“%c”输出为一个字符时,也只取其低字节的内容进行输出,忽略高位字节的内容。

2.输出整数

printf 函数输出整数的格式字符有很多个,有%d(十进制)、%u(无符号)、%o(八进制)、%x或%X(十六进制),对这些格式字符,还可添加标志、宽度、长度等修饰符。

【程序6-6】printf函数输出整数示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:      int i=75,j=-65355;
        6:
        7:      printf("整型数:%6d和%6d,输出为无符号数:%12u和%12u\n",i,j,i,j);
        8:      printf("整型数:%6d和%6d,输出为八进制数:%#12o和%#12o\n",i,j,i,j);
        9:      printf("整型数:%6d和%6d,输出为十六进制数:%#10x和%#10x\n",i,j,i,j);
        10:     getch();
        11:     return 0;
        12: }

编译执行以上程序,将得到如图6-7所示的结果。

图6-7 【程序6-6】执行结果

以上程序将变量i和j分别按无符号数、八进制数和十六进制数输出。第7行程序输出无符号数,变量i的值是正数,所以其输出结果不变。而对于变量j,其输出结果就不一样了。本书第4章曾介绍过,计算机保存负数是使用补码的方式来存储的。当使用“%u”格式字符输出变量j的值时,printf会将其作为无符号数来输出。变量j的值为-65355,其二进制补码为11111111 11111111 00000000 10110101(按int类型4字节保存),其十六进制为FF FF 00 B5,转换为十进制为4 294 901 941。

第8行程序使用“%o”格式字符输出整数,该格式字符将采用无符号数的形式输出八进制数。所以对于变量j,实际输出的是十进制为4 294 901 941转换为八进制数的结果。在格式字符中使用了修饰符“#”,所以在八进制数中将输出前缀0。

第9行程序使用“%x”格式字符输出十六进制数,使用了修饰符“#”,所以,在十六进制数前面输出前缀“0x”。

3.输出实数

printf函数输出实数的格式字符有很多个,最常用的是%f,还有%(e 或%E)、%g(或%G,对这些格式字符,也可添加标志、宽度、长度等修饰符。

【程序6-7】printf函数输出实数示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:      float f=130.79069388;
        6:      double d=13198317000.2682882;
        7:
        8:      printf("f=%f,%5.4f,%e,%g\n",f,f,f,f);
        9:      printf("d=%f,%8.4f,%E,%G\n",d,d,d,d);
        10:     getch();
        11:     return 0;
        12: }

编译执行以上程序,得到如图6-8所示结果。

图6-8 【程序6-7】执行结果

第8行程序将float类型的数据分别用“%f”、“%e”、“%g”格式进行输出。对于变量f,将其按“%f”格式输出时,将保留6位小数,在这里不是输出为“130.790693”(直接截尾)或“130.790694”(四舍五入),而是“130.790695”。这是因为,实数保存在内存中时也是使用二进制数,对二进制数进行舍入得来的值,即使是“130.79069088”(小数第6位为0),输出的也是“130.790695”。接着使用“%5.4f”格式字符控制输出的小数位数为4位,整数部分不会改变。使用“%e”按科学计数法输出,使用“%g”自动选择使用“%f”或“%e”的格式输出,变量f的值在使用“%f”格式输出的范围之内。

第9行程序输出double类型的变量d,在使用“%G”格式输出时,因其值的表示范围超出了“%f”,因此按“%E”格式输出。

4.输出字符串

printf函数的格式字符“%s”用于输出字符串,有关字符串的处理,参见本书第13章。在这里,读者只需了解字符串是由一对双引号括起来的字符组合(单引号括起的为字符)。

在printf函数中,使用“%s”格式将字符串原样输出,如果字符串中包含有转义字符,则将按转义字符的意义进行操作。如字符串中包含“\n”,则进行换行,包含“\t”后面的内容将在下一个制表位输出。

可以添加以下修饰字符,来控制字符串的输出:

%ms,输出的字符串占m列,如字符串本身长度大于m,则突破m的限制,将字符串全部输出。若串长小于m,则在左侧补空格。

%-ms,如果串长小于m,则在m列范围内,字符串向左对齐,右侧补空格。

%m.ns,输出占m列,但只输出字符串中左侧n个字符。这n个字符输出在m列右侧,左侧补空格。

%-m.ns,其中mn含义同上,n个字符输出在m列范围的左侧,右侧补空格。如果n>m,则m自动取n值,即保证n个字符正常输出。

例如,以下程序将字符串常量输出到屏幕上。

【程序6-8】printf函数输出字符串示例

        1:  #include <stdio.h>
        2:  #define STR "Hello,China!"
        3:
        4:  int main()
        5:  {
        6:      printf("%s*\n",STR);
        7:      printf("%15s*\n",STR);
        8:      printf("%-15s*\n",STR);
        9:      printf("%15.5s*\n",STR);
        10:     printf("%-15.5s*\n",STR);
        11:     getch();
        12:     return 0;
        13: }

编译执行以上程序,得到如图6-9所示结果。

图6-9 【程序6-8】执行结果

第2行程序定义字符串常量,供后面的代码使用。

第6行程序使用“%s”默认格式输出字符串。为了观察字符串输出结尾的情况,在后面添加了一个星号(*),后面几行程序也是如此。

第7行程序设置输出的宽度为15,因常量STR的字符串只有12个字符,所以,在左侧添加3个空格。

第8行程序设置左对齐,在右侧添加空格(星号将出现在添加的3个空格之后)。

第9行和第10行使用“.n”输出字符串左侧5个字符,分别用右对齐和左对齐方式输出。

在上一章中曾介绍过,可以将一个语句分成几行书写。例如,以下代码

        printf("使用printf函数输出字符串的例子,输出字符串为:%s*\n",STR);

可改写为以下样式:

        printf("使用printf函数输出字符串的例子,输出字符串为:%s*\n",
        STR);

printf函数双引号中的为一个字符串,如果将字符串拆分为两行或多行:

        printf("使用printf函数输出字符串的例子,
        输出字符串为:%s*\n",STR);

编译程序将会提示错误。

如果必须在源代码中对字符串拆行,可使用以下三种方法:

使用多个printf语句,分别输出字符串中的各部分;

在拆行的末尾跟上一个斜线(\),表示当前行字符串还未结束,下一行的内容仍然是字符串中的内容。

采用字符串连接的方法,这是ANSI C的新方法。这种方法将两个相邻或仅由空白分隔的、用双引号括起来的字符串作为一个字符串。

以下两行将是一个合法的语句:

        printf("使用printf函数输出字符串的例子,\
        输出字符串为:%s*\n",STR);

在第1行的末尾,使用斜线表示字符串还未结束,下一行的字符仍然是字符串的一部分。这里需要注意,第2行的内容必须顶格输入,否则,第2行前面添加的空格仍然是字符串的一部分。

以下两行语句使用两个双引号表示同一个字符串,这时,不需要在第1行的末尾添加斜线字符。

            printf("使用printf函数输出字符串的例子,"
            "输出字符串为:%s*\n",STR);

6.1.5 动态设置输出宽度和精度

在printf函数中,通过在格式字符前面添加相应的修饰符可控制输出最小宽度和精度。在一般情况下,都是在程序编码阶段设置好这两个参数,例如:

            printf("%15.5f*\n",f);

以上语句设置输出变量f时,至少占用15个字符的宽度,小数位数保留5位。这是常用的表示形式。其实,对于输出宽度和精度,还可以使用*号来代替,这时,printf函数将使用输出列表中对应的数据作为输出宽度和精度,其格式如下:

            printf("%*.*f*\n",m,n,f);

其中的变量(或表达式的值)m 对应第 1 个*号,设置输出宽度。变量(或表达式的值)n对应第2个*号,设置输出的精度。

【程序6-9】printf函数动态设置输出宽度和精度示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:      int m,n;
        6:      float f=130.79069388;
        7:
        8:      printf("设置数据最少输出宽度值:");
        9:      scanf("%d",&m);
        10:     printf("设置数据输出的小数位数:");
        11:     scanf("%d",&n);
        12:     printf("%*.*f\n",m,n,f);
        13:     getch();
        14:     return 0;
        15: }

编译执行以上程序,分别输入对应的值,可设置变量f的输出宽度和精度。图6-10列出了执行两次该程序,分别输入不同值得到的结果。第 1 次设置最少输出宽度值为5,小数位数为3;第2次设置宽度为15,小数位数为6。

图6-10 【程序6-9】执行结果

6.1.6 printf函数的返回值

C 函数一般都有一个返回值,返回值是由函数返回给调用程序的一个值。printf 函数也有一个返回值,正常情况下,返回打印输出的字符数目,如果printf函数执行出错,则返回一个负数。

大多数程序都不处理 printf 函数的返回值。在一些特殊环境下,需要检测 printf 函数是否执行成功,可使用一个变量来保存其返回值。

下面的程序演示了获取printf函数返回值的方法。

【程序6-10】printf函数的返回值示例

        1:  #include <stdio.h>
        2:  #define STR "Hello,China!"
        3:
        4:  int main()
        5:  {
        6:      int i;
        7:
        8:      i=printf("%s\n",STR);
        9:      printf("上一行代码输出了%d个字符!\n",i);
        10:     getch();
        11:     return 0;
        12: }

编译执行以上程序,得到如图6-11所示结果。

图6-11 【程序6-10】执行结果

第8行调用printf函数,输出字符串常量STR,并返回输出字符的数量。第9行打印上一行代码输出的字符数量。字符串常量STR保存的可见字符为12个,C将自动为字符串中添加一个结束标志,因此共有13个字符。

6.1.7 理解输出列表

printf 函数的输出列表可以有多个表达式,其数量至少应与格式字符串中的格式字符数量相匹配。若多于格式字符的数量,多余表达式的值将不会输出;若少于格式字符的数量,则可能输出一些无意义的值。例如:

            printf("%f,%d\n",f,i);

以上语句的格式字符串中有2个格式字符,输出列表中需要有2个变量或表达式。printf函数将输变量f和i的值。如果按以下方式书写:

            printf("%f,%d\n",f);

以上语句也可通过编译,函数printf输出变量f的值后,还需要输出一个整型的值,但其输出列表中却没有提供对应的值,这时,将会得到一个不可预知的、无意义的值。

以下语句:

            printf("%f,%d\n",f,i,j);

有2个格式字符,输出列表中有三个变量。这时,函数printf将只输出变量f和i的值,变量j的值将被舍弃。

使用函数 printf 输出数据时,编译器将传递给函数 printf 的参数存入一个称为堆栈的内存区域。堆栈中的数据采用“后进先出”的方式,即最后保存的数据将先读出来。因此,对于函数printf的参数列表,总是将其参数按从后到前的方法存放到堆栈中。例如:

            printf("%f,%d\n",f,i,j);

调用以上语句时,先将变量 j 的值存入堆栈,再将变量 i 的值存入堆栈,再将变量 f的值存入堆栈,最后将第1个参数(格式字符串)存入堆栈。进入函数printf执行其代码时,首先对第1个参数(格式字符串)进行分析,每找到一个格式字符,就从堆栈中取出一个输出值。这样,若格式字符的数量和输出表达式数量匹配,则函数printf能顺利输出所有值。若格式字符的数量比输出表达式的数量多,函数printf在取完装入堆栈中的值后,还将继续从堆栈中取值,所以,将得到一个无意义的数。若格式字符的数量比输出表达式的数量少,则多余的值将被留在堆栈中,不进行处理。例如,下面的程序输出变量i的值。

【程序6-11】printf函数输出列表示例1

        1:  #include <stdio.h
        2:
        3:  int main()
        4:  {
        5:      int i=3;
        6:
        7:      printf("%d\t%d\t%d\t%d\n",++i,--i,i--,i++);
        8:      getch();
        9:      return 0;
        10: }

编译执行以上程序,得到如图6-12所示的结果。

图6-12 【程序6-11】执行结果

对于初学者很难理解,为什么输出这些不同的结果。利用上面介绍的知识,就不难理解其结果了。执行第7行的程序时,按从右向左的顺序,将参数存入堆栈中。首先,将计算最右侧表达式“i++”为3,将其存入堆栈,再对变量i自增1得到4;接着处理倒数第2个表达式“i--”,表达式的值就是变量i的值4,将其存入堆栈,再对变量i自减1得到3;接着处理倒数第3个表达式“--i”,该表达式中变量i为前置自减运算,表达式和变量的值都为2,将表达式的值2存入堆栈;最后处理表达式“++i”,表达式和变量i的值都为3,将表达式的值3存入堆栈。执行函数printf时,将从堆栈中读出这些值进行输出,最后得到如图6-12所示的结果。

下面再看一个例子,当格式字符和输出列表中值的类型不匹配时,将会输出杂乱的、无意义的数据,具体参见下面的程序。

【程序6-12】printf函数输出列表示例2

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:      int i=10;
        6:      long l=90;
        7:      float f=6.18;
        8:      double d=9.18;
        9:
        10:     printf("%e,%e,%e,%e\n",i,l,f,d);
        11:     printf("%ld,%ld\n",i,l);
        12:     printf("%ld,%ld,%ld,%ld\n",f,d,i,l);
        13:     getch();
        14:     return 0;
        15: }

编译执行以上程序,得到如图6-13所示结果。

图6-13 【程序6-12】执行结果

第10行程序将变量d按double类型存入堆栈(占8个字节),接着将变量f按float类型存入堆栈(本来float只占4个字节,但C编译器会将其转换为double保存,占用8个字节),接着将变量l按long类型存入堆栈(占4个字节),最后将变量i按int类型存入堆栈(占4个字节)。堆栈示意图如图6-14所示。

图6-14 堆栈示意图

在使用printf输出这些数据时,遇到的第1个格式字符是“%e”,printf期望输出一个double类型的值。这时,将从堆栈顶端读入8 个字节。堆栈顶端的8 个字节保存的是int型变量i和long型变量l的值,将这8个字节的内容作为一个double实数输出,所以得到一个无意义的值。接着处理第2个格式字符“%e”,这时栈顶指针指向变量f开头的字节,因此,将输出以此栈顶指针开头的8个字节的内容,得到变量f的值(按程序代码应该是输出变量l的值)。接着处理第3个格式字符“%e”,这时,栈顶指针指向变量d开头的字节,因此将输出变量d的值(按程序代码应该是输出变量f的值)。到这里,已将调用printf时存入堆栈中的参数用完,对于最后一个格式字符“%e”,printf仍然要从堆栈中取出8个字节的内容来输出,得到一个无意义的值。

第11行程序中的格式字符和输出变量的类型相匹配,所以能得到正确的输出结果。

第12行程序中,最后两个变量i和l与printf中的最后两个格式字符相匹配,为什么没有得到正确的结果?参照第8行程序的分析,请读者自行分析。

6.2 格式化输入——scanf函数

6.1节介绍了将计算机内存中的数据输出到屏幕的函数printf。在前面的各程序中,都是使用处理源程序中预先输入的数据。在更多的情况下,需要计算机与用户进行交互,即由用户输入数据,经过计算机处理后,再输出结果。

C 函数库中包含多个输入函数,函数 scanf 是最常用的一个,使用该函数可以接收用户输入的各种格式的数据。本节介绍scanf函数的用法。

6.2.1 scanf函数的格式

scanf函数称为格式输入函数,使用该函数,可按指定的格式从键盘上把数据输入到指定的变量之中。

scanf 函数是一个标准库函数,它的函数原型在头文件“stdio.h”中。scanf 函数的一般形式为:

          scanf("格式字符串",地址表列);

其中,格式字符串的作用与printf函数相同。地址表列中给出各变量的地址。地址是由地址运算符“&”后跟变量名组成的。例如,&a,&b分别表示变量a和变量b的地址。这个地址是编译系统在内存中给变量分配的地址,用户不用关心具体的地址是多少。只需要在scanf函数的变量名前面加上符号“&”即可。

注意

scanf中要求给出变量地址,如给出变量名则会出错。这是初学者最容易犯的一个错误。

例如,下面的程序接收用户输入,并将输入结果显示在屏幕上。

【程序6-13】scanf函数示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:      int a,b;
        6:
        7:      printf("请输入两个整数:");
        8:      scanf("%d%d",&a,&b);
        9:
        10:     printf("您输入的两个整数a=%d,b=%d\n",a,b);
        11:     getch();
        12:     return 0;
        13: }

编译执行以上程序,将显示提示信息,根据提示信息输入数据,得到如图6-15所示结果。

图6-15 【程序6-13】执行结果

scanf函数不能显示提示信息,一般都需要使用printf函数,显示一个字符串,提示用户输入的数据种类。以上程序第5行就完成这种提示功能。

第8行程序使用了2个格式字符,分别对应两个整型变量a和b。C怎样把输入的内容分别保存到两个变量中呢?scanf函数使用空白字符(空格、换行、制表符)来分隔输入的数据,如图6-15中输入的123和456之间使用了一个空格分开,scanf就将123保存到变量a中,将456保存到变量b中。

下面更进一步地讲解scanf函数读取用户输入数据的过程。

首先简单介绍一下缓冲区的概念。用户通过键盘输入的字符并不是直接传给 scanf 函数,而是保存在一个输入缓冲区中,scanf函数按顺序从输入缓冲区中获取数据。

在第8行的程序中,使用了格式字符“%d”来获取用户输入的一个整数。scanf函数从输入缓冲区中读取一个字符,如果是一个数字或正负号,scanf函数将保存该字符,并继续读取缓冲区中的下一个字符,当遇到非数字或正负号的字符时,scanf函数认为该整数的输入结果,将最后读取的非数字符号返回输入缓冲区,供后面的输入函数使用,最后将前面读入的字符组合成一个整数,保存到对应的变量中。如果一开始就遇到空白字符(空格、换行、制表符),scanf函数将跳过这些字符,继续从缓冲区中获取下一个字符进行判断。

所以,在执行以上程序时,在输入数字之前,可以按任意多的空格(或回车、或“Tab”)而不影响输入的内容。重新执行【程序6-13】,如图6-16左图所示,在输入数字123之前,输入很多空格,在123之后,也输入很多空格,scanf将跳过这些空格,仍然将123保存在变量a中,将456保存在变量b中。

图6-16 【程序6-13】执行结果

再次执行【程序6-13】,在输入提示时,多次按回车键换行后,输入数据123,接着,按多次回车键换行后输入数据456,scanf函数将跳过这些回车符,得到同样的结果,如图6-16右图所示。

在程序提示输入时,输入“123a456”(在 123 和 456 之间插入非数字字符),仍然可以得到正确的结果。

6.2.2 scanf函数格式字符串

scanf函数中格式字符串的作用与printf函数相同,与printf函数相比,少了一个精度控制修饰符,其格式字符串的一般形式为:

        %[*][输入数据宽度][长度]类型

其中有方括号[]的项为可选项。下面分别介绍各项的意义。

1.类型

表示输入数据的类型,其格式字符和意义如下:

d或i:输入十进制整数

o:输入八进制整数

x或(X):输入十六进制整数

u:输入无符号十进制整数

f、e、g:输入实型数(用小数形式或指数形式)

c:输入单个字符

s:输入字符串

2.长度

长度格式符为 l 和 h,l 表示输入长整型数据(如%ld)和双精度浮点数(如%lf),h表示输入短整型数据。

3.宽度

用十进制整数指定输入的宽度(即字符数)。达到指定宽度后,后面输入的数据将赋值给下一个变量。

例如:

          scanf("%4d",&a);

如果输入以下内容:

          123456789

scanf函数将只把1234保存到变量a中,其余部分被舍弃。

如果有多个格式字符使用宽度限制,如以下代码:

          scanf("%3d%3d",&a,&b);

同样输入“12345678”,scanf函数将123保存到变量a中,将456保存到变量b中,其余部会舍弃。

4.星号(*)

在格式字符中使用星号(*),表示从缓冲区中读入该数据项后不赋予相应的变量,即跳过该输入值。

例如,以下语句:

          scanf("%d%*d%d",&a,&b);

当输入为:12 34 56时,scanf函数将输入的第一个数12保存到变量a中,将第二个数34跳过,最后,将第三个数56保存到变量b中。这种方式在处理已有的一批数据,而又需要跳过部分数据时很有用,可使用星号(*)跳过这些不需要用的数据。

6.2.3 scanf函数注意问题

上面介绍了 scanf 函数的格式字符,在使用格式字符时,还需要注意很多问题。下面列出初学者容易犯错误的一些地方。

1.不能控制精度

与printf函数不同,scanf函数输入实数时,不能控制精度。例如,不能使用以下代码控制输入的实数有2位小数:

            scanf("%5.2f",&f);

执行以下程序可以看到效果。

【程序6-14】scanf函数不能控制输入精度示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:      float f1,f2;
        6:
        7:      printf("请输入实数f1:");
        8:      scanf("%5f",&f1);
        9:
        10:     printf("请输入实数f2:");
        11:     scanf("%5.2f",&f2);
        12:
        13:     printf("f1=%f,f2=%f\n",f1,f2);
        14:     getch();
        15:     return 0;
        16: }

编译执行以上程序,输入第一个实数,按回车键后,将直接跳过第 13 行的程序,显示输入的内容。执行结果如图6-17所示。

图6-17 【程序6-14】执行结果

第8行程序从输入缓冲区中获取5个字符宽度的输入内容34.56,将其保存到变量f1中,第11行程序设置了不正确的修饰符,将跳过该行代码,直接执行第13行的输出语句,得到如图6-17所示的结果:变量f1为输入的值,实数有误差;变量f2的值为0(或其他不确定的值)。

2.在格式字符串中包含非格式字符

前面介绍了在输入多个数值数据时,若格式字符串中没有非格式字符作输入数据之间的间隔,则可用空白字符(空格、“Tab”键或回车键)作间隔。scanf在碰到空白字符或非法数据(如对“%d”输入“12F”时,“F”为非法数据;而对“%x”输入“12F”时,“F”是十六进制中的一个字符,不为非法字符)时,即认为该数据结束。

当格式字符串中包含非格式字符时,该怎么输入数据?如果格式字符串中有非格式字符,则要求用户输入数据时,也必须原样输入这些非格式字符。

例如:以下程序在scanf的格式字符串中使用了非格式字符。

【程序6-15】scanf函数使用非格式字符示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:      int a,b;
        6:
        7:      printf("请输入两个整数:");
        8:      scanf("a=%db=%d",&a,&b);
        9:
        10:     printf("您输入的整数a=%d,b=%d\n",a,b);
        11:     getch();
        12:     return 0;
        13: }

编译执行以上代码,按图6-18左图所示的样式输入,可使变量a和b得到正确的结果。否则用其他方式输入(如右图所示),将使变量a和b的值为不确定结果。

图6-18 【程序6-15】执行结果

第8行程序中,设置scanf格式字符串中,使用了非格式字符“a=”和“b=”,用户必须原样输入这些字符,才能正确地完成数据的输入。

又如,使用以下输入语句:

        scanf("%d,%d",&a,&b);

要求用户输入数据时,在两个数据之间用逗号隔开。

而以下语句(两个%d之间有一个空格):

        scanf("%d %d",&a,&b);

要求用户输入数据时,至少在两个数据之间要输入一个空格。

在输入字符数据时,若格式字符串中无非格式字符,则认为所有输入的字符均为有效字符。

例如:

          scanf("%c%c%c",&a,&b,&c);

输入为(字母之间有空格):

          w y h

则把字符'w'保存到变量a中,空格保存到变量b中,字符'y'保存到变量c中。只有当输入为以下内容(字符之间无空格):

          wyh

才能把字符'w'保存到变量a中,字符'y'保存到变量b中,字符'h'变存到变量c中。

如果在格式控制中加入空格作为间隔,如:

          scanf ("%c %c %c",&a,&b,&c);

则输入时,各数据之间可加空格,也可不加空格。

6.2.4 scanf函数的返回值

scanf函数也有返回值,其返回值为成功读入数据的一个数。若在scanf函数中需要读取一个整数,而用户却输入一串字符时,scanf函数将不能获得一个有效的数据,将返回0。例如,以下程序将提示用户输入数据,并显示用户输入的有效数据个数。

【程序6-16】scanf函数返回值示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:      int a,b,c,i;
        6:
        7:      printf("请输入三个整数:");
        8:      i=scanf ("%d%d%d",&a,&b,&c);
        9:
        10:     printf("您输入的整数a=%d,b=%d,c=%d\n",a,b,c);
        11:     printf("共输入了%d个有效数据!\n",i);
        12:     getch();
        13:     return 0;
        14: }

编译执行以上程序,输入2个整数和一些乱字符,得到如图6-19所示结果。

图6-19 【程序6-16】执行结果

第8行程序中,用变量i保存scanf函数的返回值。执行以上程序时,输入如图6-19所示的数据,scanf函数将34保存到变量a中,接着将343保存到变量b中,最后scanf函数还需要获取一个整数,但是在输入缓冲区中,却没有整数,因此变量c将为一个不确定的值。

第10行程序输出有效输入数据的数量,若3个变量都获取了有效数据,则其输出应该为3。

提示

除了使用scanf函数和printf函数进行输入/输出外,在C的标准函数库中,还有 gets、getchar、puts、putchar 等函数可用来进行输入/输出操作,这些函数的使用方法将在第13章中进行介绍。

6.3 其他常用输入/输出函数

本章前面介绍了格式化输入函数scanf和格式化输出函数printf,这两个函数通过格式字符可输入或输出各种类型的数据。在 C 语言中,还提供了几个常用的输入/输出函数,本节简单介绍这些函数的用法。

6.3.1 getchar函数

getchar函数的功能是接收键盘输入的一个字符。其函数原型如下:

          int getchar(void);

该函数不需要任何参数,从输入缓冲区中获取一个字符,并将其ASCII码作为返回值。通常,定义一个字符变量来接收getchar函数的返回值,例如:

        char c;
        c=getchar();

当调用 getchar 函数时,程序就等着用户按键,用户输入的字符被存放在输入缓冲区中,直到用户按回车键才结束。getchar函数返回用户输入的第一个字符的ASCII码。若执行过程中出错,该函数将返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前,输入了不止一个字符,其他字符会保留在键盘缓存区中,供后续函数读取。也就是说,当输入缓冲区中有未取完的字符时,后续的函数将不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为止,才等待用户按键。

例如,下面的程序等待用户输入字符,按回车键后,再回显一个字符,若用户输入多个字符,也只显示一个字符。

【程序6-17】getchar函数示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:       char c;
        6:
        7:       printf("输入一个字符:");
        8:       c=getchar();
        9:
        10:      printf("输入的字符是:%c\n",c);
        11:      getch();
        12:      return 0;
        13: }

6.3.2 getch函数

getch函数与getchar函数功能基本相同,都是接收用户输入一个字符。其函数原型如下:

        int getch(void)

getch函数直接从键盘获取键值,不等待用户按回车键,只要用户按一个键,getch函数就立刻返回。getch函数返回值是用户输入的ASCII码,出错,则返回-1。

使用getch函数输入的字符不会回显在屏幕上。因此,getch函数常用于程序调试中,在调试时,在关键位置显示有关的结果,以待查看,然后用 getch 函数暂停程序运行,当按任意键后,程序继续运行。

还要注意的是,getchar函数的函数原型位于stdio.h头文件中,而getch函数的函数原型位于conio.h头文件中。

例如:以下程序提示用户按任意键继续,用户按键后(不需要按回车键),将在下一行显示所按的字符。

【程序6-18】getch函数示例

        1:  #include <stdio.h>
        2:  #include <conio.h>
        3:
        4:  int main()
        5:  {
        6:       char c;
        7:
        8:       printf("按任意键继续!\n");
        9:       c=getch();
        10:
        11:      printf("按的键是:%c\n",c);
        12:      getch();
        13:      return 0;
        14: }

6.3.3 gets函数

gets函数用来从输入缓冲区中读取字符串,直至接收到换行符时停止,并将读取的结果存放在 str 指针所指向的字符数组中。换行符不作为读取字符串的内容,读取的换行符被转换为null值,并由此来结束字符串。

有关数组和指针的相关内容,参见本书第9章和第12章中的相应内容。

其函数原型如下:

        char *gets(char *string);

函数的参数string可以为一个字符指针或一个字符数组。如果是字符数组,则应该确保string的空间足够大,以便在执行读操作时,不发生溢出。

例如,下面的程序可接收用户输入的字符串,然后将其显示在屏幕中。对于程序中的数组,学习本书后面的内容后,将会明白其具体含义,这里只需按原样输入、编译即可。

【程序6-19】gets函数示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:       char string[80];
        6:
        7:       printf("请输入一个字符串:");
        8:       gets(string);
        9:
        10:      printf("输入的字符串是:%s\n",string);
        11:      getch();
        12:      return 0;
        13: }

以上程序中,在第5行定义了一个字符数组,其长度为80,表示最多可保存用户输入的80个字符。第8行使用gets函数接收用户输入的字符串。第10行将用户输入的内容输出到屏幕上。

编译执行以上程序,在提示符中,输入一个字符串(可包含空格),按回车键后,该字符串将显示在屏幕上,如图6-20所示。

图6-20 【程序6-19】执行结果

用scanf函数的格式字符“%s”,也可接收用户输入的字符串。但是,如果输入字符串中包含有空格,scanf函数将只接收空格前的数据,空格后的数据仍将保留在输入缓冲区中。而gets函数则可将用户输入的全部内容保存到字符数组中。所以,一般接收用户输入字符串时,使用gets函数更好。

6.3.4 putch函数

putch函数用于向屏幕中输出字符,其函数原型包含在conio.h头文件中,具体格式如下:

          int putch(int ch);

例如,下面的语句输出小写字母a:

        putch('a');      //输出小写字母a

而下面的语句输出字符变量c所保存的字符:

        putch(c);

使用putch,还可以输出控制字符(执行控制功能),下面的语句将执行换行操作:

        putch('\n');

对控制字符,putch则执行控制功能,不在屏幕上显示。

例如,下面的程序使用多个putch函数输出一个字符串。

【程序6-20】putch函数示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:       char c1='G',c2='o',c3='o',c4='d';
        6:
        7:       putch(c1);
        8:       putch(c2);
        9:       putch(c3);
        10:      putch(c4);
        11:      getch();
        12:      return 0;
        13: }

在stdio.h头文件中,还包含一个名为putchar的函数,也可用来向屏幕上输出一个字符。在使用上,与putch函数没什么区别。

6.3.5 puts函数

puts函数用来输出一个字符串到屏幕上。与使用printf函数的格式字符“%s”完成的功能相同。该函数的函数原型如下:

          int puts(char *string);

与gets函数类似,参数string可为字符数组或字符指针。

下面的例子演示使用gets接收用户输入的字符串及使用puts函数输出字符串的效果。

【程序6-21】puts函数示例

        1:  #include <stdio.h>
        2:
        3:  int main()
        4:  {
        5:       char str[80];
        6:
        7:       printf("请输入一个字符串:");
        8:       gets(str);
        9:
        10:      puts("输入的字符串是:");
        11:      puts(str);
        12:      getch();
        13:      return 0;
        14: }

以上程序中,第8行接收用户输入的字符串,并将其保存到字符数组str中。第10行输出一个字符串常量,第11行输出字符数组str中保存的字符串。

初学者容易犯的一个错误如下:

        char c='w';
        puts(c);

在上面的程序中,打算使用puts函数,输出字符变量c的值。当执行上面的代码时,程序将出现错误。这是因为字符串必须以结束字符'\0'作为结束标志,而字符变量c只包含一个字符,没有结束标志,所以不能使用pusts函数来输出。