
2.6 了解如何声明函数
编译器看到我们要调用某个函数时,为了正确处理函数调用操作,它必须了解这是一个什么样的函数。换句话说,它先把这个函数的定义处理清楚,然后才知道如何处理函数调用操作。大家目前看到的程序都是按照这样的顺序编写的,我们总是先定义需要调用的函数,然后在程序的其他函数里面调用这个函数。
这样写其实有点死板,因为这样我们就不能在还没有编写函数定义的情况下提前调用这个函数。假如我们要这样调用,那就得把后来所写的函数定义上移到调用这个函数的地方之前,让编译器能够先看到函数的定义,然后再处理函数调用。但我们不应该总是这样迁就编译器,而是应该想办法让编译器来适应我们。
为此,C语言提供了一种机制,让我们先声明函数,让编译器通过声明充分了解与该函数有关的信息,以后再处理这个函数的完整定义。这样的声明就叫作函数声明(function declaration)。这种声明只需要把函数的返回类型、名称以及参数列表告诉编译器,三者结合即是我们早前提到的函数签名(或者叫作函数特征)。你在后面针对该函数所写的定义,必须在返回类型、名称以及参数列表这三个方面与早前的声明相符,而且还需要给出函数的语句块(也就是说,需要把函数的主体定义出来)。声明函数时所用的函数签名必须跟你定义这个函数以及调用这个函数时所用的相符。如果函数的声明与其定义不符,那么编译器就会报错。许多编程错误都是由这个原因所导致的。
函数声明也叫作函数原型(function prototype),这种称呼要比前者更容易同函数定义区分。然而这并不是强制要求,这两种叫法都是对的。但是在本书中,我们还是采用函数原型这个说法,因为函数声明比较容易同函数定义(function definition)混淆。下面这个hello7.c程序在开头部分书写了5个函数原型:


hello7.c程序把这几个函数的定义顺序也调整了一下,让它们按照受调用的先后次序出现。这样的实现方式称为自顶向下的实现方式[1](top-down implementation),先受到调用的函数总是定义在后受到调用的函数之前。比方说,main()函数受到调用的时机比其他函数都早,因此它在整个程序里面是最先定义的。printANewLine()函数是最后一个受到调用的,因此,它定义在整个源文件的最后。这种定义顺序与把大问题拆解成多个小问题时所用的思路是相当接近的。与之相比,旧版程序所采用的实现方式叫作自底向上的实现方式[2](bottom-up implementation),这种方式在定义函数时所用的顺序与程序调用这些函数时的顺序恰好相反,程序里面最先受到调用的函数(也就是main()函数)在源文件里面最后一个得到定义。这两种实现方式都可以写出正确的程序。
如果我们想采用自顶向下的实现方式,那么必须编写函数原型,以让编译器正确地处理这些函数。C语言只要求函数的原型必须出现在调用这个函数的那条语句之前,至于多个函数原型之间的顺序,则没有要求。
为了编写hello7.c程序,你可以复制hello6.c,并把新的文件保存为hello7.c,然后重新排列这几个函数的定义顺序,同时给文件开头补上相应的函数原型。你也可以不这样做,而是从头开始,把刚才那段代码照原样录入计算机。这两种办法都可以。写完hello7.c文件之后,编译这个程序。你还可以试着把文件开头的#include<stdio.h>拿掉,看看这样编译hello6.c与hello7.c是不是会产生同样的错误。
新版程序的执行顺序并没有变化。虽然hello7.c文件修改了函数的定义顺序,但函数之间的执行顺序依然与hello6.c相同,因此,针对这个程序所画的执行顺序图其实与2.5节的那张图是相同的。
请你在hello7.c中按照刚才那段代码所写的顺序来定义这些函数,让main()函数的定义出现在其他函数之前,然后把相应的函数原型添加到文件开头。接下来编译并运行程序,最后验证该程序的输出结果与hello6.c程序相同。
各函数的原型应该集中放置在文件开头。这不是强制要求,但最好这样写。
函数原型之间的顺序不一定要跟这些函数得到定义的顺序相同。但笔者建议你还是应该确保两者一致,这样做虽然有点枯燥,但能够让你在寻找函数定义时更加方便,比方说,如果能确保两者的顺序一致,那么当你发现function_C()的原型位于function_B()之后,且在function_D()与function_E()之前时,你就能够肯定,这个函数的定义也排在function_B()的定义之后,并且排在function_D()与function_E()的定义之前。这样,我们就可以把放置函数原型的这段代码当作一个索引,从而迅速找到某个函数在源文件中的定义。
学会声明函数原型,你就可以在main()函数之外定义任意数量的函数。只要这些函数的原型正确,你就可以按照自己想要的顺序来定义这些函数,并在程序里面调用它们。
[1] 也叫由上而下的实现方式、从上到下的实现方式。——译者注
[2] 也叫由下而上的实现方式、从下到上的实现方式。——译者注