1.函数定义
函数的定义就是函数体的实现。函数体就是一个代码块,它在函数被调用时执行。与函数定义相反,函数声明出现在函数被调用的地方。函数声明向编译器提供该函数的相关信息,用于确保函数被正确地调用。
形式参数列表包括变量名和它们的类型声明。代码块包含了局部变量的声明和函数调用时需要执行的语句。
2.函数声明
当编译器遇到一个函数调用时,它产生代码传递参数并调用这个函数,而且接受该函数返回的值。
1)原型:向编译器提供一些关于函数的特定信息显得更为安全,可以通过两种方法来实现。首先如果同一源文件的前面已经出现了该函数的定义,编译器就会记住它的参数数量和类型,以及函数的返回值类型;接着编译器便可以检查该函数的所有后续调用(在同一源文件中),确保它们是正确的。第2种向编译器提供函数信息的方法是使用函数原型,原型总结了函数定义的起始部分的声明,向编译器提供有关该函数应该如何调用的完整信息。使用原型最方便(且最安全)的方法是把原型置于一个单独的文件,当其他源文件需要这个函数的原型时,就使用#include指令包含该文件。
a)现在函数原型具有文件作用域,所以原型的一份拷贝可以作用于整个源文件,较之在该函数每次调用前单独书写一份函数原型容易得多;
b)现在函数原型只书写一次,这样就不会出现多份原型的拷贝之间的不匹配现象;
c)如果函数的定义进行修改,我们只需修改函数原型,并重新编译所有包含了该原型的源文件即可;
d)如果函数的原型同时也被#include指令包含到定义函数的文件中,编译器就可以确认函数原型与函数定义的匹配;
通过只书写函数原型一次,消除了多份原型的拷贝间不一致的可能性。
2)函数的缺省认定:当程序调用一个无法见到原型的函数时,编译器便认为该函数返回一个整型值。对于那些并不返回整型值的函数,这种认定可能会引起错误。所有的函数都应该具有原型,尤其是那些返回值不是整型的函数。
3.函数的参数
C函数的所有参数均以“传值调用”方式进行传递,这意味着函数将获得参数值的一份拷贝。C的规则很简单:所有参数都是传值调用。数组参数的这种行为似乎与传值调用相悖,但是此处其实并无矛盾之处——数组名的值实际上是一个指针,传递函数的就是这个指针的一份拷贝。下标引用实际上是间接访问的另一种形式,它可以对指针执行间接访问操作,访问指针指向的内存位置。
4.ADT和黑盒
C可以用于设计和实现抽象数据类型(ADT),因为它可以限制函数和数据定义的作用域,这个技巧也被称为黑盒(black box)设计。限制对模块的访问是通过static关键字的合理使用实现的,它可以限制对那些并非接口的函数和数据的访问。
5.递归
C通过运行时堆栈支持递归函数的实现,递归函数就是直接或间接调用自身的函数。
1)追踪递归函数:追踪一个递归函数执行过程的关键是理解函数中所声明的变量是如何存储的,当函数被调用时,它的变量的空间是创建于运行时堆栈上的,以前调用函数的变量仍保留在堆栈上,但它们被新函数的变量所掩盖,因此是不能被访问的。
2)递归与迭代:递归是一种强有力的技巧,但和其它技巧一样,它也可能被误用。递归所需要的两个特性:存在限制条件,当符合这个条件时递归便不再继续;每次递归调用之后越来越接近这个限制条件。
6.可变参数列表
1)stdarg宏:这个头文件声明了一个类型va_list和三个宏——va_start、va_arg和va_end。
2)可变参数的限制:注意,可变参数必须从头到尾按照顺序逐个访问。
7.警告的总结
1)错误地在其它函数的作用域内编写函数原型;
2)没有为那些返回值不是整型的函数编写原型;
3)把函数原型和旧式风格的函数定义混合使用;
4)在va_arg中使用错误的参数类型,导致未定义的结果;
8.编程提示的总结
1)在函数原型中使用参数名,可以给使用该函数的用户提供更多的信息;
2)抽象数据类型可以减少程序对模块实现细节的依赖,从而提高程序的可靠性;
3)当递归定义清晰的优点可以补偿它的效率开销时,就可以使用这个工具;