原型在什么地方会失败
我们需要考虑4种情况
1.K&R C函数声明和K&R C函数定义
能够顺利调用,所传递的参数会进行类型提升
2.ANSI C函数声明(原型)和ANSI函数定义
能够顺利调用,所传递的参数为实际参数。
3.ANSI C函数声明(原型)和K&R C函数定义
如果使用一个较窄的类型就会失败!函数调用时所传递的是实际类型,而函数期望接受的是提升后的类型。
4.K&R C函数声明和ANSI C函数定义
如果使用一个较窄的类型就会失败!函数调用是所传递的是提升后的类型,而函数期望接受的是实际类型。所以,如果为一个K&R C函数定义增加函数原型,而原型的参数列表中有一个short参数,在参数传递时,这个原型将导致实际传递给函数的就是short类型的参数,而根据函数的定义,他期望接受的是一个int类型的参数。可以通过在原型中强迫使用宽类型,从而使代码在第3、4种两种情况下仍能正常运行。但这种做法不仅违背了可移植性原则,而且会给维护代码的程序员带来困惑。
/*
** ANSI C function prototype and K&R C function definition.
** K&R C function declaration and ANSI C function definition.
*/
#include <stdio.h>
#include <stdlib.h>
void printf_ver( short s, char ch );
void printf_ver2();
int main( void ){
short s;
char ch;
s = 225;
ch = 'a';
printf_ver( s, ch );
/*printf_ver2( s, ch );*/
return EXIT_SUCCESS;
}
void printf_ver( s, ch ) short s; char ch;{
printf( "%d %c\n", s, ch );
}
/*
** [Error] conflicting types for 'printf_ver2'
** [Note] an argument type that a default promotion can't match an empty parameter name list declaration
** [Note] previous declaration of 'printf_ver2' was here
** void printf_ver2( short s, char ch ){
** printf( "%d %c\n", s, ch );
** }
*/
void printf_ver2( s, ch ) short s; char ch;{
printf( "%d %c\n", s, ch );
}
输出:
文件1
/*旧风格的函数定义,但它却具有原型*/
int olddef(d, i)
float d;
char i; {
printf("olddef: float = %f, char = %x\n", d, i);
}
/*新风格的定义,但它却没有原型*/
int newdef(float d, char i) {
printf("newdef:float = %f, char = %x\n", d, i);
}
文件2
/*旧风格的定义,但它具有原型*/
int olddef(float d, char i);
int main() {
float d = 10.0;
char j = 3;
olddef(d, j);
/*新风格的定义,但它没有原型*/
newdef(d, j);
}
注意,如果把函数的定义放在它们被调用的同一个文件内,程序的行为就会不一样。编译器将会检测到olddef()的不匹配,因为它现在看不到原型和K&R C的函数定义。如果把newdef()的定义放在它被调用之前,编译器就会平静地执行正确的操作,因此此时函数的定义就相当于原型,它保证了声明和定义的一致性。如果把函数的定义放在它被调用以后,编译器就会发出“类型不匹配”的错误信息。由于C++要求所有函数必须具有原型,你可能会想用C++编译器去编译那些旧式的K&R C代码,在编译器发出错误信息的地方逐个为函数添加原型。
早些时候我曾提到原型允许编译器检查函数使用和声明之间的一致性。在实际编程中,我们通过把函数原型放置在头文件中,而把函数定义放置在另一个包含了该头文件的源文件中来防止这种情况的发生。编译器可以同时发现它们,如有不匹配就能检测到。
不要在函数的声明和定义中混用新旧两种风格
如果函数在头文件里的声明是K&R C风格的,那么该函数的定义也应该使用K&R C风格的语法。
int foo(); int foo(a, b) int a; int b; {/*...*/}
如果函数在头文件里的声明是ANSI C风格的,那么该函数的定义也应该使用ANSI C风格的语法。
int foo(int a, int b); int foo(int a, int b) {/*...*/}
可以建立一种可靠的机制来检查跨越多个文件的函数调用。在printf这种参数不定的函数中需要使用特别的技巧(当前就是如此)。它甚至可以应用于现存的语法。它所需要的就是在标准中规定每次调用函数时必须在参数名字、数量、类型以及函数的返回地址上与函数的定义保持一致。这种“预防艺术”确实存在,Ada语言就是这么做的。C语言也可以使用这种方法,不过需要在链接器之前进行一个额外的传递(重要提示:使用lint程序)
或许ANSI C委员会在这方面也应该做一番努力,规定一个完整的解析过程,即使它在链接器之前需要进行一次传递。应该摒弃C++那种烦琐的部分解析过程,并且规定自己的约定、语法、语义和限制。
/*
**编程挑战。
*/
在一个独立的文件里创建下列函数:
void banana_peel( char a, short b, float c ){
printf( "char = %c, short = %d, float = %f\n", a, b, c );
}
在另一个独立的文件里,建立调用banana_peel()的主程序。
1.试试在使用原型和不使用原型这两种情况下调用它,再试试在原型和定义不匹配的情况下调用它。
/*file.c内容如下:*/
#include <stdio.h>
void banana_peel( char a, short b, float c ){
printf( "char = %c, short = %d, float = %f\n", a, b, c );
}
/*file2.c
*调用banana_peel的程序。*/
#include <stdio.h>
#include <stdlib.h>
int main( void ){
banana_peel( 'b', 2, 6.0 );
return EXIT_SUCCESS;
}
/*
**file3.c
**调用banana_peel的程序。
*/
#include <stdio.h>
#include <stdlib.h>
extern void banana_peel( char a, short b, float c );
int main( void ){
banana_peel( 'b', 2, 6.0 );
return EXIT_SUCCESS;
}
/*
**file4.c
**调用banana_peel的程序。
*/
#include <stdio.h>
#include <stdlib.h>
extern void banana_peel( int a, short b, char c );
int main( void ){
banana_peel( 'b', 2, 6.0 );
return EXIT_SUCCESS;
}
没有使用原型的情况下的输出:
使用原型的情况下输出:
原型和定义不匹配的情况下输出:
2.在每种情况下,在运行代码之前预测结果。编写一个union,你可以向它存储一个值却取回另一个值(两个值的长度不同),检测你的预测是否正确。
与原型有关的类型提升
#include <stdio.h>
int main() {
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);
return 0;
}
输出:
3.参数次序的改变(在函数的声明和定义中)是否会影响被调用函数接收参数的方式?解析其中的原因。你的编译器捕捉到多少种错误情况?
解析:(modification)
会影响被调用函数接收参数的方式。因为参数次序的改变会导致参数被相应的提升和裁剪。