七、函数
7.1 函数定义
函数定义就是函数体的实现。函数体就是个代码块,它在函数被调用时执行。函数的定义语法如下:
类型
函数名(形式参数)
代码块
function_name()
{
}
当这个函数被调用时,它简单地返回。然而,它可以实现一种有用的存根(stub)目的,为那些此时尚未实现的代码保留一个位置。
return语句
当执行流达到函数定义的末尾时,函数就将返回(return),也就是说执行流返回函数被调用的地方。
7.2 函数声明
编译器是如何知道该函数期望接受的是什么类型和数量的参数呢?如何知道到该函数的返回值(如果有的话)类型呢?
7.2.1 原型
向编译器提供一些关于函数的特定信息显然更为安全,我们可以通过两种方法来实现。首先,如果同一源文件的前面已经出现了该函数的定义,编译器就会记住它的参数数量和类型,以及函数的返回值类型。
第二种是向编译器提供函数信息的方法的使用函数原型(function prototype)。使用原型最方便的方法是把原型置于一个单独的文件,当其他源文件需要这个函数的原型时,就是用#include指令包含该文件。举个例子:
int *find_int (int key, int array[], int len);
注意最后面那个分号:它区分了函数原型和函数定义的起始部分。
7.2.2 函数的缺省认定
当程序调用一个无法见到原型的函数时,编译器便认为该函数返回一个整数值。对于那些并不返回整型值的函数,这种认定可能会引起错误。
警告:
所有的函数都应该具有原型,尤其是那些返回值不是整型的函数。记住,值的类型并不是值的内在本质,而是取决于它被使用的方式。
7.3 函数的参数
c函数的所有参数均以“传值调用”方式进行传递,这意味着函数将获得参数值的一份拷贝。这样,函数就可以放心修改这个拷贝值,而不必担心会修改调用程序实际传给它的参数。
但是在传递数组的时候,函数修改的实际上的调用程序的数组元素。因为数组名的值是一个指针,它可以对指针执行间接访问操作,访问指针指向的内存位置。
记住两个规则:
1.传递给函数的标量参数是传值调用的。
2.传递给函数的数组参数在行为上就像它们是通过传址调用那样。
程序7.3a 整数交换:无效版本
void swap(int x , int y)
{
int temp;
temp = x;
x = y;
y = temp;
}
程序7.3b 整数交换:有效版本
void swap(int *x , int *y)
{
int temp;
temp = x;
x = y;
y = temp;
}
因为函数期望接受的参数是指针,所以我们应该按照下面的方式调用它:
swap(&a , &b);
7.4 ADT和黑盒
C可以用于设计和实现抽象数据类型(ADT, abstract data type),因为它可以限制函数和数据定义的作用域。这个技巧也被称为黑盒设计。抽象数据类型的基本想法是很简单的—-模块具有功能说明和接口说明,前者说明模块所执行的任务,后者定义模块的使用。
限制模块的访问是通过static关键字的合理使用实现的,它可以限制对那些并非接口的函数和数据的访问。
黑盒的概念使实现细节与外界隔绝,这就消除了用户试图直接访问这些实现细节的诱惑。这样,访问模块唯一可能的方法就是通过模块定义的接口。
7.5 递归
C通过运行时堆栈支持递归函数的实现。递归函数就是直接或间接调用自身的函数。用计算阶乘和斐波那切数列说明递归,这是不幸的。前者,递归并没有任何优越之处。后者,它的效率之低是非常恐怖的。
标准并没有说明需要堆栈,但是,堆栈非常适合于实现递归,所以许多编译器都使用堆栈来实现递归。
7.5.1 追踪递归函数
追踪一个递归函数执行过程的关键是理解函数中所声明的变量是如何存储的。当函数被调用时,它的变量的空间是创建于运行时堆栈上的。以前调用的函数的变量仍保留在堆栈上,但它们被新函数的变量区分开来,以避免混淆。
当递归函数调用自身时,情况也是如此。
7.5.2 递归与迭代
递归是一种强有力的技巧,但和其他技巧一样,它也可能被误用。这里就有一个例子。
/*
**用递归方法计算n的阶乘
*/
long factorial(int n)
{
if(n <= 0)
return 1;
else
return n*factorial(n - 1);
}
/*
**用迭代方法计算n的阶乘
*/
long factorial(int n)
{
int result = 1;
whlie(n > 1){
result *= n;
n -= 1;
}
return result;
}
提示:
许多问题是以递归的形式进行解释的,这只是因为它比非递归形式更为清晰。但是,这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性可能稍微差一些。当一个问题相当复杂,难以用迭代形式实现时,此时递归实现的简洁性便可以补偿它所带来的运行时的开销。
/*
**用递归方法计算第n个斐波那契数的值
*/
long fibonacci( int n)
{
if( n <= 2)
return 1;
return fibonacci( n -1) + fibonacci(n - 2);
}
/*
**用迭代方法计算第n个斐波那契数的值
*/
long fibonacci( int n)
{
long result;
long previous_result;
long next_older_result;
result = previous_result = 1;
while(n > 2){
n -= 1;
next_older_result = previous_result;
previous_result = result;
result = previous_result + next_older_result;
}
return result;
}
7.6 可变参数列表
/*
**计算指定数目的值的平均值(差的方案)
*/
float average( int n_values, int v1, int v2, int v3, int v4, int v5)
{
float sum = v1;
if(n_values >= 2)
sum += v2;
if(n_values >= 3)
sum += v3;
if(n_values >= 4)
sum += v4;
if(n_values >= 5)
sum += v5;
return sum/n_values;
}
7.6.1 stdarg 宏
可变参数列表是通过宏来实现的,这些宏定义与stdarg.h头文件。这个头文件声明了一个类型va_list和三个宏—-va_start、va_arg和va_end。我们可以声明一个va_list的变量,与这几个宏配合使用,访问参数的值。
7.6.2 可变参数的限制
可变参数必须从头到尾按照顺序逐个访问。如果你在访问了几个可变参数后想半途中止,这是可以的。但是,如果你想一开始就访问参数列表中间的参数,那是不行的。
/*
**计算指定数量的值的平均值
*/
#include <stdarg.h>
float average(int n_values,...)
{
va_list var_arg;
int count;
float sum = 0;
/*准备访问可变参数*/
va_start(var_arg,nvalues);
/*添加取自可变参数列表的值*/
for(count = 0; count < n_values; count += 1)
{
sum += va_arg(var_arg, int );
}
/*完成处理可变参数*/
va_end(var_arg);
return sum / n_values;
}
1.这些宏无法判断实际存在的参数的数量
2.这些宏无法判断每个参数的类型。
7.8 警告的总结
- 错误的在其他函数的作用域内编写函数原型。
- 没有为那些返回值不是整数的函数编写原型。
- 把函数原型和旧式风格的函数定义混合使用。
- 在va_arg中使用错误的参数类型,导致未定义的结果。
7.9 编程提示的总结
- 在函数原型中使用参数名,可以给使用该函数的用户提供更多的信息.
- 抽象数据类型可以减少程序对模块实现细节的依赖,从而提高程序的可靠性。
- 当递归定义的清晰的优点可以补偿它对效率开销时,就可以使用这个工具。