7.2 函数声明
当编译器遇到一个函数调用时,它产生代码传递参数并调用这个函数,而且接收该函数返回的值(如果有的话)。但编译器是如何知道函数期望接受的是什么类型和多少数量的参数呢?如何知道该函数的返回值(如果有的话)类型呢?
如果没有关于调用函数的特定信息,编译器便假定在这个函数调用时参数的类型和数量是正确的。它同时会假定函数将返回一个整型值。
对于那些返回值并非整型的函数而言,这种隐式认定常常导致错误。
7.2.1 原型
向编译器提供一些关于函数的特定信息显然更为安全,我们可以通过两种方式来实现。首先,如果同一源文件的前面已经出现了该函数的定义,编译器就会记住它的参数数量和类型,以及函数的返回值类型。其次,编译器便可以检查该函数的所有后续调用(在同一个源文件中),确保它们是正确的。
K&R C:
如果函数是以旧式风格定义的,也就是用一个单独的列表给出参数的类型,那么编译器就只记住函数的返回值类型,但不保存函数的参数数量和类型方面的信息。由于这个缘故,只要有可能,就应该使用新式风格的函数定义,这点非常重要。
第二种像编译器提供函数信息的方法是使用函数原型(function prototype)。原型总结了函数定义的起始部分的声明,向编译器提供有关该函数应该如何调用的完整信息。使用原型最方便(且最安全)的方法是把原型置于一个单独的文件,如果其他源文件需要这个函数的原型,就使用#include指令包含该文件。这个技巧避免了错误输入函数原型的可能性,又简化了程序的维护任务,因为这样只需要该原型的一份物理副本。如果原型需要修改,只需要修改它的一处副本即可。
int *find_int( int key, int array[], int len );
注意最后面的那个分号:它区分了函数原型和函数定义的起始部分。原型告诉编译器函数的参数数量和每个参数的类型以及返回值的类型。编译器见过原型之后,就可以检查该函数的调用,确保参数正确且返回值无误。当出现不匹配的情况时,编译器会把不匹配的实参或返回值转换为正确的类型,当然前提是这样的转换必须是可行的。
提示:
注意上面的原型中加上了参数的名字。虽然它并非必须,但在函数原型中加入描述性的参数名是明智的,因为它可以给希望调用该函数的用户提供有用的信息。例如:
char *strcpy( char *, char * );
char *strcpy( char *destination, char *source );
警告:
下面的代码段说明了一种使用函数原型的危险方法。
void a()
{
int *func( int *value, int len );
}
void b()
{
int func( int len, int *value );
}
两个函数的参数的顺序倒了,返回类型也不同。问题在于这两个函数原型都写于函数体内部,它们都具有代码块作用域。所以编译器在每个函数结束前会把它记住的原型信息丢弃,这样它就无法发现它们之间存在的不匹配情况。
标准表示,在同一个代码块中,函数原型必须与同一个函数的任何先前原型匹配,否则编译器应该生成一条错误信息。但是在这个例子里,第1个代码块的作用域并不与第2个代码块重叠,因此,原型的不匹配就无法检测到。这两个原型至少有一个是错误的(也可能两个都错),但编译器看不到这种情况,所以不会生成任何错误信息。
下面的代码段说明了一种使用函数原型的更好方法:
#include "func.h"
void a()
{
...
}
void b() {
...
}
文件func.h包含了下面的函数原型:
int *func( int *value, int len );
从几个方面看,这个技巧比前一种方法更好。
1.现在函数原型具有文件作用域,所以原型的一份副本可以作用于整个源文件,较之在该函数每次调用前单独书写一份函数原型要容易得多。
2.现在函数原型只书写一次,这样就不会出现多份原型的副本之间不匹配的现象。
3.如果函数的定义进行了修改,我们只需要修改原型,并重新编译所有包含了该原型的源文件即可。
4.如果函数的原型同时也被#include指令包含到定义函数的文件中,编译器就可以确认函数原型与函数的定义匹配。
通过只书写函数原型一次,我们消除了多份原型的副本之间不一致的可能性。然而,函数原型必须与函数定义匹配。把函数原型包含在定义函数的文件中可以使编译器确认它们之间的匹配性。
看上去有些含糊:
int *func();
它既可以看作一个旧风格的声明(只给出了func函数的返回类型),也可以看作一个没有参数的函数的新风格原型。它究竟是哪一个呢?这个声明必须被解释为旧式风格的声明,目的是保持与ANSI标准之前的程序的兼容性。一个没有参数的函数的原型应该写成下面这个样子:
int *func( void );
关键字void表示没有任何参数,而不是表示它有一个类型为void的参数。
7.2.2 函数的缺省认定
当程序调用一个无法见到原型的函数时,编译器便认为该函数返回一个整型。对于那些并不返回整型的函数,这种认定可能会引起错误。
警告:
所有的函数都应该具有原型,尤其是那些返回值不是整型的函数。记住,值的类型并不是值的内在本质,而是取决于它被使用的方式。如果编译器认定函数返回一个整型值,它将产生整数指令操纵这个值。如果这个值实际上是个非整型值,比如说是个浮点值,其结果通常将是不正确的。
xyz是一个函数,它返回浮点值3.14。
用于表示这个浮点数的二进制模式如下:
01000000010010001111010111000011
现在假定函数是这样被调用的:
float f;
...
f = xyz();
如果在函数调用之前编译器无法看到它的原型,它便认定这个函数返回一个整型值,并产生指令将这个值转换为浮点值,然后再赋值给变量f。
函数返回的位如上所示。转换指令把它们解释为整型值1078523331,并把这个值转换为float类型,结果存储于变量f中。
为什么函数的返回值实际上已经是浮点值的形式时,还要执行类型的转换呢?编译器并没有办法知道这个情况,因为没有原型或声明告诉它这些信息。这个例子说明了为什么返回值不是整型的函数具有原型是非常重要的。
C和指针 第7章 函数 7.2 函数声明
于 2022-06-17 20:21:18 首次发布