关于C语言函数的原型声明与函数定义,这里涉及了许多的内容,以此文章,记录自己所了解的知识,以备日后查阅,同时也帮助自己和大家了解这当中暗含的“陷阱”。
由于历史的原因,C语言的函数声明有旧式和新式之分,旧式就是K&R,而新式则是ANSI,如下图:
现在倡导的是使用后者,而不要使用前者,对于K&R,由于存在大量旧式代码,为了保持兼容,所以没有被正式废弃。
这两者在参数传递的时会有所区别,在K&R中,由于函数的参数也是表达式,所以会发生类型提升,即传递一个短于int的整数,函数实际所接收到的是int,如果传递的是float,函数实际接收到的是double,在被调用函数的函数体内,这些值会根据函数定义时参数的声明类型自动裁减为该类型。
你可能会感到困惑,为什么不嫌麻烦将它们提升为更大的类型,然后又直接把它们裁减为原来的大小?之所以这样做,原意是为了简化编译器—所有的东西都是同一长度。如果只固定使用几种类型,将大大简化参数的传递。所有的参数都统一为标准长度,被调用函数会根据需要对它们进行裁剪。
相反在ANSI中,如果使用了适当的函数原型,类型提升便不会发生,如果参数声明为char,则实际传递的也是char。使用新风格的函数定义,编译器就会假定参数是准确声明的,于是便不进行类型提升,并据此产生代码。
关于声明和定义,我们需要考虑4种情况
- K&R C函数声明和K&R C函数定义
能够顺利调用,所传递的参数会进行类型提升 - ANSI C函数声明(原型)和ANSI C函数定义
能够顺利调用,所传递的参数位实际参数 - ANSI C函数声明(原型)和K&R C函数定义
如果使用一个较窄的类型就会失败,函数调用时所传递的是实际类型,而函数期望接收的是提升后的类型 - K&R C函数声明和ANSI C函数定义
如果使用一个较窄的类型就会失败,函数调用时所传递的是提升后的类型,而函数期望接收的是实际类型
所以,如果为一个K&R C函数定义增加函数原型,而原型的参数列表中有一个short参数,在参数传递时,这个原型将导致实际传递给函数的就是short类型的参数,而根据函数的定义,它期望接收的是一个int类型的参数。这样,函数从堆栈中抓取4个字节(int)而不是2个字节(short)。
下面展示两种失败情况
/* 文件1 */
/* 旧风格的定义,但它具有原型 */
int olddef (float d, char i);
int main(void)
{
float d = 10.0;
char j = 3;
olddef (d, j);
/* 新风格的定义,但它没有原型 */
newdef (d, j);
}
----------------------------------------------------------------
/* 文件2 */
/* 旧风格的定义,但它具有原型 */
olddef(d , i)
float d;
char i;
{
printf ("olddef: float = %f, char = %x \n", d, i);
}
/* 新风格的定义,但它没有原型 */
newdef(float d, char i)
{
printf ("newdef: float = %f, char = %x \n", d, i);
}
打印结果如下
所以,坚决不要在函数的声明和定义中混用新旧两种风格。
还有一种错误的用法:
int main(void)
{
union
{
double d;
float f;
}u;
u.d = 10.0;
printf ("put in a double, pull out a float f = %f \n", u.f);
u.f = 10.0;
printf ("put in a float, pull out a double d = %f \n", u.d);
}
打印的结果为: