预计阅读本页时间:-
在ANSI C规范之前的传统的函数声明形式是不够准确的,因为它只声明了函数的返回值类型,而没有声明其参数。下面我们看一下使用旧的函数声明形式时所产生的问题。
下面的ANSI之前形式的声明通知编译器imin()返回一个int类型的数值:
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
然而,该语句并没有说明imin()的参数个数和类型。因此,如果在函数imin()中使用错误的参数类型或参数个数不对,编译器就不能发现这种错误。
9.2.1 产生的问题
下面我们讨论几个使用imax()函数的例子,该函数和imin()类似。在程序清单9.4中的程序以旧的形式声明函数imax(),然后错误地使用该函数。
程序清单9.4 misuse.c程序
在第一个printf()中调用imax()时漏掉了一个参数,而在第二次调用imax()时使用了浮点参数而不是整数参数。尽管存在这些错误,该程序仍可以编译执行。
以下是使用Metrowerks Codewarrior Development Studio 9 时的输出:
Digital Mars 8.4将生成数值4202837和1074266112。使用两种编译器都可以编译通过,只不过它们因为程序没有使用函数原型而产生了错误。
程序运行时发生了些什么?因为各操作系统的内部机制不同,所以出现以上错误的具体情况也不相同。当使用PC或VAX时,程序执行过程是这样的:调用函数首先把参数放在一个称为堆栈(stack)的临时存储区域里,然后被调函数从堆栈中读取这些参数。但是这两个过程并没有相互协调进行。调用函数根据调用过程中的实际参数类型确定需要传递的数值类型,但是被调函数是根据其形式参数的类型进行数据读取的。因此,函数调用imax(3)把一个整数放在堆栈中。当函数imax()开始执行时,它会从堆栈中读取两个整数。而实际上只有一个需要的数值被存储在堆栈中,所以第二个读出的数据就是当时恰好在堆栈中的其他数值。
第二次使用函数imax()时,传递的是float类型的数值。这时两个double类型的数值就被放在堆栈中(回忆一下,作为参数传递时float类型数据会被转换成double类型数据)。而在我们使用的系统中,这意味着两个64位的数值,即共128位的数据存储在堆栈中。因为这个系统中的int类型是32位,所以当imax()从堆栈中读取两个int类型的数值时,它会读出堆栈中前面64位的数据,把这些数据对应于两个整数,其中较大的一个就是1074266112。
9.2.2 ANSI的解决方案
针对以上的参数错误匹配问题,ANSI标准的解决方案是在函数声明中同时说明所使用的参数类型。即使用函数原型(function prototype)来声明返回值类型、参数个数以及各参数的类型。为了表示imax()需要两个int类型的参数,可以使用下面原型中的任意一个进行声明:
第一种形式使用逗号对参数类型进行分隔;而第二种形式在类型后加入了变量名。需要注意的是这些变量名只是虚设的名字,它们不必和函数定义中使用的变量名相匹配。
使用这种函数原型信息,编译器就可以检查函数调用语句是否和其原型声明相一致。比如检查参数个数是否正确,参数类型是否匹配。如果有一个参数类型不匹配但都是数值类型,编译器会把实际参数值转换成和形式参数类型相同的数值。例如,会把imax(3.0, 5.0)换成imax(3, 5)。当使用函数原型时,上例中的程序清单9.4会变成如下的程序清单9.5。
程序清单9.5 proto.c程序
当编译程序清单9.5时,编译器会给出一个错误信息,声称调用函数imax()时传递的参数太少。
当存在类型错误时会出现什么结果呢?为了说明这一点,我们用imax(3, 5)代替imax(3)后重新进行编译。这一次并没有出现任何错误信息。执行程序后结果如下:
正如上文所述,第二次调用时3.0和5.0被转换成3和5,因此被调函数就可以对传入的数据进行正确处理。
虽然编译中没有出现错误信息,但是编译器给出了一条警告信息,提示doube类型数据被转换成了int类型的数据,因此可能会损失数据。例如,以下函数调用:
等价于语句:
错误和警告的不同之处在于前者阻止了编译的继续进行而后者不阻止。一些编译器进行这个类型转换,但不显示警告信息,因为C标准并没有要求进行警告提示。不过,大多数编译器允许用户通过选择警告级别来控制编译器在显示警告时的详细程度。
9.2.3 无参数和不确定参数
假设使用以下函数原型:
这时一个ANSI C编译器会假设您没有用函数原型声明函数,它就不会进行参数检查。因此,为了表示一个函数确实不使用参数,需要在圆括号内加入void关键字:
ANSIC会把上句解释为print_name()不接受任何参数,因此当对该函数进行调用时编译器就会检查以保证您确实没有使用参数。一些函数(比如printf()和scanf())使用的参数个数是变化的。例如在printf()中,第一个参数是一个字符串,而其余参数的类型以及参数个数并不固定。对于这种情况,ANSI C允许使用不确定的函数原型。例如,对于printf()可以使用下面的原型声明:
这种原型表示第一个参数是一个字符串(在第11章“字符串和字符串函数”中详细解释了这一知识点),而其余的参数不能确定。
对于参数个数不确定的函数,C库通过stdarg.h头文件提供了定义该类函数的标准方法。本书第16章“C预处理器和C库”详细讲述了有关内容。
9.2.4 函数原型的优点
函数原型是对语言的有力补充。它可以使编译器发现函数使用时可能出现的错误或疏漏。而这些问题如果不被发现的话,是很难跟踪调试出来的。您可以不使用函数原型,而使用旧的函数声明形式(不说明参数的函数声明),但是这么做不仅没有任何优势反而存在许多缺点。
有一种方法可以不使用函数原型却保留函数原型的优点。之所以使用函数原型,是为了在编译器编译第一个调用函数的语句之前向其表明该函数的使用方法。因此,可以在首次调用某函数之前对该函数进行完整的定义。这样函数定义部分就和函数原型有着相同的作用。通常对较小的函数会这样做: