任何C函数都有一个形参列表,列表中的每个参数都是一个变量,该变量在函数调用过程中被初始化。下面这个函数有一个整型形参:
int abs(int n) { return n<0?-n;n; }
而对某些函数来说,形参列表为空。例如,
void eatline() { int c; do c = getchar(); while(C != EOF && c!’n’); }
函数调用时,调用方将实参列表传递给被调函数。在下面的例子中,a-b是传递给函数abs的实参:
if(abs(a-b)>n) printf(“difference is out of range\n”);
一个函数如果形参列表为空,在调用时实参列表也为空。例如,
eatline();
任何一个C函数都有返回类型,要么是void,要么是函数和成结果的类型。函数的返回类型理解起来比参数类型相对容易一些,因此我们将首先讨论它。
如果任何一个函数在调用它的每个文件中,都在第一次被调用之前乾了声明或定义,那么就不会有任何与返回类型相关的麻烦。例如,考虑下面的例子,函数square计算它的双精度类型参数的平方值:
double square(double x) { return x*x; }
以及,一个调用square函数的程序:
main() { printf(“%g\n”, square(0.3)); }
要使这个程序能够运行,函数square必须要么在main之前进行定义:
double square(double x) { return x*x; } main() { printf(“%g\n”, square(0.3)); }
要么在main之前进行声明:
double square(double); main() { printf(“%g\n”, square(0.3)); } double square(double x) { return x*x; }
如果一个函数在被定义或声明之前被调用,那么它的返回类型就默认为整型。上面的例子中,如果将main函数单独抽取出来作 业个源文件:
main() { printf(“g\n”, square(0.3)); }
因为函数main假定函数square返回类型为整型,而函数square返回类型实际上是双精度类型,当它与square函数连接时就会得出错误的结果。
如果我们需要在两个不同的文件中分别定义函数main与函数square,那么应该如何处理呢?函数square只能有一个定义。如果square的调用与定义分别位于不同 文件中,那么我们必须在调用它的文件中声明square函数。
double square(double); main() { printf(“g\n”, square(0.3)); }
C语言中形参与实参匹配的规则稍微有一点复杂。ANSI C允许程序员在声明时指定函数的参数类型:
double square(double);
上面的语句说明函数square接受一个双精度类型的参数,返回一个以精度类型的结果。根据这个声明,square(2)是合法的;整数2将会被自动转换为双精度类型,就好像程序员写成square((double)2)或者square(2.0)一样。
如果一个函数没有float、short或者char类型的参数,在函数声明中完全可以省略参数类型的说明(注意,函数定义中不能省略参数类型的说明)。因此 ,即使在ANSI C中,像下面这样声明square函数也是可以的:
double square();
这样做依赖地调用者能够提供数目正确且类型且类型恰当的实参。这里,“恰当”并不意味着“等同”;float类型的参数会自动转换为double类型,short或char类型的参数公自动转换为int类型。例如,对于下面的函数:
int isvowel(char c) { return c==’a’||c==’e’||c==’i’||c==’o’||c=’u’; }
因为其形参为char类型,所以在调用该函数的其他文件中必须声明:
int isvowel(char);
否则,调用都将传递给isvowel函数的实参自动转换为int类型,这样就与形参类型不一致了。如果函数isvowel是这样定义的:
int isvowel(int c){ return c==’a’||c==’e’||c==’i’||c==’o’||c=’u’; }
那么调用者就无需进行声明,即使调用者在调用时传递给isvowel函数一个char类型的参数也是如此。
ANSI C标准发布之前出现的C编译器,并不都支持这种风格的声明。当我们使用这类编译器时,有必要如下声明isvowel函数:
int isvowel();
以及这样定义它:
int isvowel(c) char c; { return c==’a’||c==’e’||c==’i’||c==’o’||c=’u’; }
为了与早期的用法兼容,ANSI C也支持这种较“老”形式的声明和定义。这就带来一个问题:如果一个文件调用了isvowel函数,却又不能声明它的参数类型(为了能够在较“老”的编译器上工作),那么编译器如何知道函数形参是char类型而不是int类型的呢?答案在于,新旧两种不同的函数定义形式,代表不同的含义。上面isvowel函数的最后一个定义,实际上相当于:
int isvowel(int i) { char c = i; return c==’a’||c==’e’||c==’i’||c==’o’||c=’u’; }
现在我们已经了解了函数定义与声明的有关细节,再来看看这方面容易出错的一些方式。下面这个程序虽然简单,却不能运行:
main() { double s; s = sqrt(2); printf(“%g\n”, s); }
原因有两个:第一个原因是,sqrt函数本应接受一个双精度值为实参,而实际上却被传递了一个整型参数;第二个原因是,sqrt函数的返回类型是双精度类型,但却并没有这样声明。
一种更正方式是:
double sqrt(double); main() { double s; s = sqrt(2); printf(“%g\n”, s); }
若用另一种方式,则更正后的程序可以在ANSI C标准发布之前就存在C编译器上工作,即:
double sqrt(); main() { double s; s = sqrt(2.0); printf(“%g\n”, s); }
当然,最好的更正方式晕样:
#include <math.h> main() { double s; s = sqrt(2.0); printf(“%g\n”, s); }
这个程序看上去并没有显式的说明sqrt函数的参数类型与返回类型,但实际上它从系统头文件math.h中获得了这些信息。尽管本例中为了与早期C编译器兼容,已经把实参写成了双精度的2.0而不是整型的2,然则即使仍然写作整型的2,在符合ANSI C的标准的编译器上,这个程序也能确保实参会被转换为恰当的类型。
因为函数printf与函数scanf 不同情形下可以接受不同类型 参数,所以它们特别容易出错。这里有一个值得注意的例子:
#include <stdio.h> main() { int i; char c; for(i=0;i<5;i++){ scanf(“%d”, &c); printf(“%d”,i); } printf(“\n”); }
表面上,这个程序从标准输入设备读入5个参数,在标准输出设备上写5个数:
0 1 2 3 4
实际上,这个程序并不一定得到上面的结果。例如,在某个编译器上,它的输出是
0 0 0 0 0 1 2 3 4
为什么呢?问题的着急在于,这里c被声明为char类型,而不是int类型。当程序要求scanf读入一个整数,应该传递给它一个指向整数的指针。而程序中scanf函数得到的却是一个指向字符的指针,scanf函数并不能分辨这种情况,它只是将这个指向字符的指针作为指向整数的指针而接受,并且在指针指向的位置存储一个整数,因为整数所占有的存储空间要大于字符所占的存储空间,所以字符c附近的内存将被覆盖。
字符c附近的内存中存储的内容是由编译器决定的,本像中它存放的是整数i的低端部分。因此,每次读入一个数值到c时,都会将i的低端部分覆盖为0,而i的高端部分本来就是0,相当于i每次被重新设置为0,循环将一直进行。当到达文件的结束位置后,scanf函数不再试图读入新的数值到c。这时,i才可以正常地递增,最后终止循环。