DONG Yuxuan @ Jun 03, 2016 CST
C语言函数声明考古报告
在 K&R C 时代,函数的声明和定义是长成这样的:
double square(); /* 声明 */
...
...
...
double square(x) /* 定义 */
double x;
{
return x * x;
}
明显这里有一个问题,那就是声明并不包含函数的参数表,这就是说编译器将允许调用者任意地传递参数,像这样:
#include <stdio.h>
double square();
main()
{
square(1, 2, 3, 4, 5);
return 0;
}
double square(x)
double x;
{
return x * x;
}
这样得到的结果自然是不正确的。
另一方面我们注意到我们的main
函数甚至连返回值都没有写,这在 K&R C 中也是可以的,其默认返回值为int
, 所以我们最后能够return 0
。
到了 C89 时代,我们就可以比较正常地声明函数原型了:
double square(double);
为了兼容性考虑,C89 也支持老式的声明(包括缺省返回值),而在C99中,函数原型声明虽然依然可以省略参数表,但是返回值类型却必须明确指出。
我们继续考虑这种 K&R C 风格的函数原型,必须指出它是有一定方便之处的,比如我们想调用库函数qsort
对一个整数数组按升序排序,那么我们需要定义一个比较函数:
int icmp(int *a,int *b) { return *a - *b; }
然而qsort
要求比较函数的两个参数是const void *
类型,在 C99 中,函数指针的隐式转换已经被禁止,所以像下面的代码是无法通过 C99 编译的:
#include <stdlib.h>
#include <stdio.h>
int icmp(int *a, int *b) { return *a - *b; }
int main(void) {
int i ,buff[] = {3 ,7 ,4 ,1 ,0 ,9 ,6};
qsort(buff ,sizeof buff / sizeof *buff, sizeof *buff, icmp);
for (i = 0; i < sizeof buff / sizeof *buff; ++i) printf("%d\n", buff[i]);
return 0;
}
这里就有两种方法,一种是我们把icmp
做一个类型转换:
qsort(
buff,
sizeof buff / sizeof *buff,
sizeof *buff,
(int (*)(const void *, const void *))icmp
);
这写起来就非常繁琐,而另一种方法自然是按照qsort
的要求来定义icmp
(这当然是工程上最佳的做法):
int icmp(const void *a, const void *b) { return *(int *)a - *(int *)b; }
但这样需要在icmp的实现里做并没有什么意义的类型转换,比较难看,要使得代码简洁一点,我们可以利用 K&R C 风格的函数声明来强制转换:
qsort(buff, sizeof buff / sizeof *buff, sizeof *buff, (int (*)())icmp);
或者直接使用 K&R C 风格来声明我们的函数,像这样:
#include <stdlib.h>
#include <stdio.h>
int icmp();
int main(void)
{
int i, buff[] = {3, 7, 4, 1, 0, 9, 6};
qsort(buff, sizeof buff / sizeof *buff, sizeof *buff, icmp);
for (i = 0; i < sizeof buff / sizeof *buff; ++i) printf("%d\n", buff[i]);
return 0;
}
int icmp(int *a, int *b) { return *a - *b; }
使用 K&R C 风格函数原型的时候务必要小心,因为 C 有一个叫做默认参数提升的性质,十分容易让程序员犯错误。
让我们先回到 K&R C 时代,还是考察我们的square
函数:
#include <stdio.h>
double square();
main()
{
printf("%f\n", square(1));
return 0;
}
double square(x)
double x;
{
return x * x;
}
输出并不是我们预想中的1。
这是因为main
函数对square
函数的参数类型一无所知,所以当我们传递1作为实参时,它并不会被自动转换为double
类型,而就是把一个int
类型变量压栈了,而在控制权移交给square
函数后,它却尝试从栈中弹出一个double
变量,这样自然不能取得我们预想的结果。
所以正确的调用方法应该是:
printf("%f\n", square(1.0));
现在我们测试另一种写法:
printf("%f\n", square(1.0f));
按照我们刚才的说法,这种调用应该不能得到预期的结果,因为压入一个float
却弹出一个double
,不过事实上这代码却能够成功输出我们想要的值。这个事实说明,对于 K&R C 风格声明的函数,在一定的情况下参数类型会发生自动的转换,这种性质被称为默认参数提升,具体来说,它是这样的一些规则:
[unsigned] char
或[unsigned] short
类型,则它会被转化为[unsigned] int
类型入栈float
类型,则它会被转化为double
类型入栈为了更完整地解释这个性质,我们考虑单精度浮点数版本的square
函数,并用 K&R C 风格进行声明:
float square(); /* 声明 */
...
...
...
float square(float x) /* 定义 */
{
return x * x;
}
由于默认参数提升的存在,(在只考虑传入浮点数参数的情况下)这个函数事实上等价于:
float square(double x)
{
float y = (float)x;
return y * y;
}
也就是说,如果我们传入一个float
类型变量,那么它首先被转换为double
类型压入栈中,而在函数内部,从栈中弹出这个double
变量后会将它重新转换回float
类型。
因此,如下的对单精度版本square
函数的调用都是正确的:
square(1.0f);
square(1.0);
另一方面,默认参数提升只指针对没有声明类型的参数发生作用,如果我们使用C89或更新标准的语法来声明函数:
float square(float); /* 声明 */
...
...
...
float square(float x) /* 定义 */
{
return x * x;
}
那么传给square的参数并不会被提升为double类型。
然而,并不是只有在 K&R C 风格的函数原型中才会出现类型不定的参数,事实上在 ANSI C (C89/C99/more) 风格的函数原型中也会出现无法确定参数信息的函数原型,这就是变参函数:
int printf(char *fmt, ...);
变参函数的不定参数依然会发生默认参数提升,这就是为什么在变参宏va_arg(va_list ap, type)
中,第二个参数type
不能指定为short
, char
, float
,因为这是不可能的,如果调用者传递的参数是这些类型,他们都会被提升,这也就解释了为什么输出double
和float
都可以用printf("%f", ...)
。