阅读 ‧ 电子书库 参数传递
参数传递的机制随实现不同而不同。下面是参数传递在我们的系统中的工作原理。函数调用如下:
阅读 ‧ 电子书库
该调用告诉计算机把变量n1、n2、n3和n4的值传递给计算机,计算机把它们放置到被称为堆栈(stack)的一块内存区域中来实现。计算机根据变量的类型而非转换说明符把这些值放到堆栈中。所以,n1在堆栈中占用8个字节(float被转换成了double)。同样,n2占用8个字节,而n3和n4则分别占用4个字节。然后控制转移到printf( )函数。该函数从堆栈把值读出来,但是在读取时,它根据转换说明符去读取。%ld说明符指出,printf( )应该读取4个字节,所以printf( )在堆栈中读取前4个字节作为它的第一个值。这就是n1的前半部分,它被解释成一个长整数(longinteger)。下一个%ld说明符再读取4个字节;这就是n1的后半部分,它被解释成第二个长整数(longinteger)(请参见图4.9)。同样,%ld的第三个和第四个实例使得n2的前半部分和后半部分被读出,并被解释成两个长整数(long integer)。所以,虽然n3和n4的说明符都正确,但是printf( )仍然读取了错误的字节。

阅读 ‧ 电子书库

图4.9 参数传递

二、printf( )的返回值

正如第2章中所提到的,C函数一般都有一个返回值。这就是由函数计算并返回给调用程序的值。例如,C函数库包含一个sqrt( )函数,它接受一个数字作为参数,并返回该参数的平方根。返回值可以赋给一个变量,也可以用在计算中,还可以作为参数传递。总之,它可以像任何其他值那样使用。printf( )函数也有一个返回值,它返回所打印的字符的数目。如果有输出错误,那么printf( )会返回一个负数(printf( )的一些老版本会有不同的返回值)。

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

printf( )的返回值是其打印输出用途的附带功能,通常很少被用到。使用返回值的一个可能原因是要检查输出错误。在向文件中输出而非向屏幕上输出的时候,这是很常用的。如果一张已满的软盘拒绝写入,那么您的程序应该采取正确的行动,例如使终端蜂鸣30秒钟。不过,在那样做之前,您必须了解if语句。程序清单4.13中的简单示例给出了确定返回值的方法。

程序清单4.13 prntval.c程序

阅读 ‧ 电子书库

阅读 ‧ 电子书库

输出如下:

阅读 ‧ 电子书库

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

三、打印较长的字符串

有时,printf( )语句会很长,以至于不能在一行被放下。因为空白字符(空格、制表符和换行符)如果不是用来分隔元素,那么C将忽略它们,所以可以在多行放置一个语句,只要在元素之间放置换行符即可。例如,程序清单4.13使用两行来放置一个语句。

阅读 ‧ 电子书库

该行在逗号和rv之间断开。为了让读者知道该行未完,示例缩进了rv。C忽略了多余的空格。

不过,您不能在引号括起来的字符串中间断行。如果您试图执行如下语句:

阅读 ‧ 电子书库

C将抱怨您在字符串常量中使用了一个非法字符。您可以在字符串中使用\n符号来表示换行字符,但是在字符串中不能通过回车(Enter或Return)键产生实际的换行字符。

如果您必须分割一个字符串,有三个选项可供选择,如程序清单4.14所示。

程序清单4.14 longstrg.c程序

阅读 ‧ 电子书库

下面是输出:

阅读 ‧ 电子书库

方法1是使用多个printf( )语句。因为打印的第一个字符串没有以\n字符结束,所以第二个字符串紧跟第一个字符串输出。

方法2是用反斜线符号(/)和回车键的组合来结束第一行。这就使得屏幕上的文本另起一行,并且在字符串中不会包含换行字符。其效果就是在下一行继续该字符串。不过,下一行必须像代码中那样从行的最左边开始。如果缩进了该行,比如说缩进了5个空格,那么这5个空格会变成字符串的一部分。

方法3采用字符串连接的方法,它是ANSI C的新方法。如果在一个用双引号引起来的字符串后面跟有另一个用双引号引起来的字符串,而且二者之间仅用空白字符分隔,那么C会把该组合当作一个字符串来处理,所以以下三种形式是等同的:

阅读 ‧ 电子书库

在所有这些方法中,您应该在字符串内部包含所有必需的空格:“young” “lovers”变成“younglovers”,但是组合“young” “lovers”就是“young lovers”。

4.4.5 使用scanf( )

现在,我们从输出转到输入,来看一下scanf( )函数。C函数库包含了多个输入函数,scanf( )是其中最常用的一个,因为它可以读取各种格式的数据。当然,从键盘输入的是文本,因为那些键生成文本字符:字母、数字和标点。比如说,当您想输入整数2004时,您键入字符2、0、0和4。如果想把它们存储成一个数值而非字符串,那么您的程序必须把这个字符串逐个字符地转换成数值,这就是scanf( )所做的工作!它把输入的字符串转换成各种形式:整数、浮点数、字符和C的字符串。它是printf( )的逆操作,后者把整数、浮点数、字符和C的字符串转换成要在屏幕上显示的文本。

跟printf( )一样,scanf( )使用控制字符串和参数列表。控制字符串指出输入将被转换成的格式。主要的区别是在参数列表中。printf( )函数使用变量名、常量和表达式;而scanf( )函数使用指向变量的指针。幸运的是,要使用该函数,不必对指针有任何了解;只需要记住下面这些简单规则:

● 如果使用scanf( )来读取前面讨论过的某种基本变量类型的值,请在变量名之前加上一个&。

● 如果使用scanf( )把一个字符串读进一个字符数组中,请不要使用&。

程序清单4.15给出了一个短程序来说明这些规则。

程序清单4.15 input.c程序

阅读 ‧ 电子书库

下面是一个交互结果的示例:

阅读 ‧ 电子书库

scanf( )函数使用空格(换行、制表符和空格)来决定怎样把输入分成几个字段。它依次把转换说明与字段相匹配,并跳过它们之间的空格。请注意我们分两行进行了输入。同样,也可以分一行或5行输入,只要在每个输入项目之间至少键入一个换行符、空格或者制表符。惟一的例外就是%c说明,即使下一个字符是空白字符,它也会读取那个字符。我们很快还会再讨论这个主题。

scanf( )函数所用的转换说明字符与printf( )所用的几乎完全相同。主要的区别在于printf( )把%f、%e、%E、%g和%G同时用于float类型和double类型,而scanf( )只把它们用于float类型,而用于double类型时要求使用1修饰符。表4.6列出了C99标准中描述的主要转换说明符。

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

 

 

转换说明符 意义
%c 把输入解释成一个字符
%d 把输入解释成一个有符号十进制整数
%e,%f,%g,%a 把输入解释成一个浮点数(%a是C99标准)
%E,%F,%G,%A 把输入解释成一个浮点数(%A是C99标准)
%i 把输入解释成一个有符号十进制整数
%o 把输入解释成一个有符号八进制整数
%p 把输入解释成一个指针(一个地址)
%s 把输入解释成一个字符串;输入的内容以第一个非空白字符作为开始,并且包含直到下一个空
白字符的全部字符
%u 把输入解释成一个无符号十进制整数
%x,%X 把输入解释成一个有符号十六进制整数

也可以在表4.6所示的转换说明符中使用修饰符。修饰符出现在百分号和转换字符之间。如果在一个说明符内使用多个修饰符,那么它们出现的顺序应该与在表4.7中出现的顺序相同。

表4.7 scanf()的转换修饰符

 

 

修 饰 符 意 义
* 滞后赋值(请参见书中的文字部分)
示例:“%*d”
digit(s) 最大字段宽度:在达到最大字段宽度或者遇到第一个空白字符时(不管哪个先发生都一样)停止对输入项的读取
示例:“%10s”
hh 把整数读作singned char或unsigned char
示例:“%hhd”“%hhu”
ll 把整数读作long long或者unsigned long long(C99)
示例:“%lld”“%llu”
h,l或L “%hd”和“%hi”指示该值将会存储在一个short int中。“%ho”、“%hx”和“%hu”指示该值将会存储在一个
unsigned short int中。“%ld”和“%li”指示该值将会存储在一个long中。“%lo”、“%lx”和“%lu”指示该值将会
存储在一个unsigned long中。“%le”、“%lf”和“%lg”指示该值以double类型存储。将L(而非l)与e、f和
g一起使用指示该值以long double类型存储。如果没有这些修饰符,d、i、o和x指示int类型,而e、f和
g指示float类型

正如您所看到的,转换说明符的使用比较复杂,而且这些表省略了一些特性。这些省略了的特性主要便于从高度格式化的源(例如穿孔卡或者其他数据记录媒介)读取选定的数据。因为本书主要是把scanf( )用作向程序提供交互式数据的一种便利方式,所以我们就不再讨论这些更深奥的特性。

一、从scanf( )的角度看输入

我们更仔细地研究scanf( )怎样读取输入。假定您使用了一个%d说明符来读取一个整数。scanf( )函数开始每次读取一个输入字符,它跳过空白字符(空格、制表符和换行符)直到遇到一个非空白字符。因为它试图读取一个整数,所以scanf( )期望发现一个数字字符或者一个符号(+或者-)。如果它发现了一个数字或一个符号,那么它就保存之并读取下一个字符;如果接下来的字符是一个数字,它保存这个数字,并读取下一个字符。就这样,scanf( )持续读取和保存字符直到它遇到一个非数字的字符。如果遇到了一个非数字的字符,它就得出结论:已读到了整数的尾部。scanf( )把这个非数字字符放回输入。这就意味着当程序下一次开始读取输入时,它将从前面被放弃的那个非数字字符开始。最后,scanf( )计算它读取到的数字的相应数值,并将该值放到指定的变量中。

如果您使用了字段宽度,那么scanf( )在字段结尾或者在第一个空白字符处(二者中最先到达的一个)终止。

如果第一个非空白字符不是数字,将会发生什么呢?比如说,是A而非一个数字?这时scanf( )会停在那里,并把A(或者不管是什么)放回输入。没有把任何值赋给指定的变量,程序下一次读取输入时,它就在A处重新开始。如果程序中只有%d说明符,scanf( )永远也不会越过那个A(去读下一个)。而且,如果您使用带有多个说明符的scanf( )语句,ANSI C要求函数在第一个出错的地方停止读取输入。

使用其他数字说明符读取输入与使用%d的情况相同。主要的区别在于scanf( )也许会把更多的字符看作数字的一部分。例如,%x说明符要求scanf( )识别十六进制数字a到f和A到F。浮点说明符要求scanf( )识别小数点、指数记数法(e-notation)、新的p记数法(p-notation)。

如果使用%s说明符,那么空白字符以外的所有字符都是可接受的,所以scanf( )跳过空白字符直到遇到第一个非空白字符,然后保存再次遇到空白字符之前的所有非空白字符。这就意味着%s使scanf( )读取一个单词,也就是说,一个不包含空白字符的字符串。如果使用字段宽度,scanf( )在字段的结尾或者第一个空白字符处停止。不能通过字段宽度使得scanf( )用一个%s说明符读取多于一个字的输入。最后一点:当scanf( )把字符串放在一个指定的数组中时,它添加终止的‘\0’使得数组内容成为一个C字符串。

如果使用%c说明符,那么所有的输入字符都是平等的。如果下一个输入字符是一个空格或者换行符,将会把这个空格或换行符赋给指定的变量;不会跳过空白字符。

实际上,scanf( )不是C最常用的输入函数。这里提到它是因为它的用途很多(它可以读取所有不同的数据类型),但是C还有其他几个输入函数,例如getchar( )和gets( )。它们更适用于专门的任务,例如读取一个字符或者读取包含空格的字符串。我们将在第7章“C控制语句:分支和跳转”、第11章“字符串和字符串函数”和第13章“文件输入/输出”中讨论这样的一些函数。同时,如果需要一个整数、小数、字符或者字符串,您都可以使用scanf( )。

二、格式字符串中的常规字符

scanf( )函数允许您把普通字符放在格式字符串中。除了空格字符之外的普通字符一定要与输入字符串准确匹配。例如,如果无意间把逗号放在两个说明符之间:

阅读 ‧ 电子书库

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

阅读 ‧ 电子书库

因为在格式字符串中逗号紧跟在%d后面,所以您必须紧跟88输入一个逗号。不过,因为scanf( )会跳过整数前面的空白字符,所以在输入时可以在逗号后面键入一个空格或换行符。也就是说,下面的两种输入方式也可以接受:

阅读 ‧ 电子书库

或者

阅读 ‧ 电子书库

格式字符串中的空格意味着跳过下一个输入项之前的任何空格。例如下面的语句:

阅读 ‧ 电子书库

将会接受下列任何一个输入行:

阅读 ‧ 电子书库

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

除了%c以外的说明符会自动跳过输入项之前的空格,所以scanf(“%d%d”,&n,&m)与scanf(“%d%d”, &n, &m)的行为是相同的。对于%c来说,向格式字符串中添加一个空格将导致一些区别。例如,如果在格式字符串中%c之前有一个空格,那么scanf( )会跳到第一个非空白字符处。也就是说,命令scanf(“%c”, &ch)读取在输入中遇到的第一个字符,而scanf(“%c”, &ch)则读取遇到的第一个非空白字符。

三、scanf( )的返回值

scanf( )函数返回成功读入的项目的个数。如果它没有读取任何项目(当它期望一个数字而您却键入了一个非数字字符串时就会发生这种情况),scanf( )会返回值0。当它检测到“文件结尾”(end of file)时,它返回EOF(EOF是在文件stdio.h中定义的特殊值。一般,#define指令把EOF的值定义为-1)。我们将会在第6章“C控制语句:循环”中讨论“文件结尾”,并在本书稍后利用scanf( )的返回值。在您学会if语句和while语句后,可以使用scanf( )返回值来检测和处理不匹配的输入。

4.4.6 printf( )和scanf( )的*修饰符

printf( )和scanf( )都可以使用*修饰符来修饰说明符的意义,但是它们的方式不同。首先,我们看看*修饰符能为printf( )做什么。

假定您不想事先指定字段宽度,而是希望由程序来指定该值,那么您可以在字段宽度部分使用*代替数字来达到目的,但是您也必须使用一个参数来告诉函数字段宽度应该是什么。也就是说,如果转换说明符是%*d,那么参数列表中应该包括一个*的值和一个d的值。该技术也可以和浮点值一起使用来指定精度和字段宽度。程序清单4.16就是表明这个工作原理的简短示例。

程序清单4.16 varwid.c程序

阅读 ‧ 电子书库

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

阅读 ‧ 电子书库

在这里,第一个问题的答案是6,所以6就是所用的字段宽度。与之类似,第二个答案指示字段宽度为8,并且小数点右边有3位数字。更一般地,一个程序应根据weight的值来决定这些变量的值。

在scanf( )中*提供截然不同的服务。当把它放在%和说明符字母之间时,它使函数跳过相应的输入项目。程序清单4.17提供了一个示例。

程序清单4.17 skip2.c程序

阅读 ‧ 电子书库

程序清单4.17中的scanf( )命令指示:跳过两个整数,并把第三个整数复制给n。下面是一个运行示例:

阅读 ‧ 电子书库

如果程序需要读取一个文件中某个特定的列(该文件的数据以统一的列排列),那么该功能将非常有用。

4.4.7 printf的用法提示

在想打印几列数据时,指定固定字段宽度是很有用的。因为默认的字段宽度是数字的宽度,所以如果同一列中的数字大小不同,那么下列语句:

阅读 ‧ 电子书库

如果被执行多次,将会输出参差不齐的列。例如,输出可能是这样的:

阅读 ‧ 电子书库

(这里假定变量的值在各个print语句之间发生了变化)。

可以通过指定足够大的固定字段宽度使输出更加整齐清晰。例如,使用下列语句:

阅读 ‧ 电子书库

产生如下结果:

阅读 ‧ 电子书库

在两个转换说明之间放一个空白字符,可以确保即使一个数字溢出了自己的字段,它也不会闯入下一个数字一起输出。这是因为控制字符串中的常规字符(包括空格)会被打印出来。

另一方面,如果语句中要嵌入一个数字,那么指定一个和期望的数字宽度同样小或者更小的字段宽度通常会比较方便。这使得数字的宽度正合适,而无需不必要的空白符。例如,下列语句:

阅读 ‧ 电子书库

可能产生:

阅读 ‧ 电子书库

把转换说明更改为%10.2f会产生如下结果:

阅读 ‧ 电子书库