第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,其中m、n含义同上,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函数来输出。