2010-12-14 66 views
4

是否声明一个头文件是必需的?此代码:是否声明头文件是必需的?

main() 
{ 
    int i=100; 

    printf("%d\n",i); 
} 

似乎工作,输出的是我得到的是100即使不使用stdio.h头文件。这怎么可能?

+0

可能重复[为什么#include 不需要使用printf()?](http://stackoverflow.com/questions/336814/why-include-stdio-h-is-not-required-to-使用printf) – vaxquis 2015-10-30 02:31:10

回答

9

这怎么可能?总之:三件运气。

这是可能的,因为有些编译器会对未声明的函数做出假设。具体来说,假设参数为int,返回类型也为int。由于int通常与char*(取决于体系结构)大小相同,因此可以通过传递int s和字符串,因为正确的大小参数将被压入堆栈。

在你的榜样,因为printf未声明,假设取两个int参数,你通过了char*int这是在调用的术语“兼容”。所以编译器耸了耸肩,生成了一些本应该是正确的代码。 (它真的应该已经警告你一个未声明的函数。)

所以第一个运气是编译器的假设与真实函数兼容。

然后在链接器阶段,因为printf是C标准库的一部分,所以编译器/链接器会自动将其包含在链接阶段。由于printf符号确实在C stdlib中,所以链接器解析了符号,一切都很好。链接是第二个运气,因为除了标准库以外的任何其他功能都需要链接它的库。

最后,在运行时我们看到你的第三块运气。编译器做出了一个盲目的假设,默认情况下该符号恰好被链接。但是 - 在运行时,您可以轻松地传递数据以使应用程序崩溃。幸运的是参数匹配起来了,正确的事情最终发生了。这肯定不会总是如此,我敢说上述可能会在64位系统上失败。

所以 - 为了回答最初的问题,包含头文件是非常重要的,因为如果它起作用,那只能是盲目的运气!

+1

一个很好的解释! – anatolyg 2010-12-14 12:26:10

14

你不要包含头文件。它的目的是让编译器知道关于stdio的所有信息,但如果你的编译器很聪明(或懒惰),那绝不是必须的。

应该包含它,因为它是一个很好的习惯进入 - 如果你不这样做,那么编译器必须知道,如果你打破了规则,比如有没有真正的方法:

int main (void) { 
    puts (7);  // should be a string. 
    return 0; 
} 

编译没有问题,但运行时正确地转储核心。它更改为:

#include <stdio.h> 
int main (void) { 
    puts (7); 
    return 0; 
} 

会导致编译器警告你喜欢的东西:

qq.c:3: warning: passing argument 1 of ‘puts’ makes pointer 
       from integer without a cast 

一个体面的编译器可能会发出警告,如gcc知道什么printf应该看起来像,甚至没有头:

qq.c:7: warning: incompatible implicit declaration of 
       built-in function ‘printf’ 
+0

paxdiablo @我使用gcc编译器本身....我使用Ubuntu 10.04 LTS – Manu 2010-12-14 11:32:56

0

由于paxidiablo说它没有必要,但这只适用于函数和变量,但如果您的头文件提供了一些使用的类型或宏(#define),那么您必须包含头文件以使用它们,因为它们是需要的前联预处理或编译期间即发生

0

这是可能的,因为当C编译器看到(你的情况的printf())未声明的函数调用它假定它有

 

int printf(...) 

签名并尝试将其转换为将所有参数转换为int类型。由于“int”和“void *”类型通常具有相同的大小,因此大部分时间都适用。但依靠这种行为是不明智的。

+0

它不会尝试将所有参数强制转换为int,它会使用更复杂的参数提升方案(char和short - > int,任何* - > void *,float - > double AFAIR),并将其视为固定arg函数声明这些论点。它不被视为可变参数'(...)'。 – Vovanium 2010-12-14 12:12:25

+0

我同意演员表的更正。有关可变参数的说明,请参阅 请参阅http://stackoverflow.com/questions/336814/why-include-stdio-h-is-not-required-to-use-printf – imaximchuk 2010-12-14 12:20:51

0

Çsupprots三种类型的函数参数的形式:

  1. 已知的固定参数:这是当你用声明的参数功能:foo(int x, double y)
  2. 未知固定参数:这是当你与空括号声明它:foo()(不能与foo(void)困惑:它是第一种形式参数),或不会宣布它。
  3. 变量参数:这是当您用省略号声明时:foo(int x, ...)

当您看到标准函数工作时,函数定义(表单1或3中)与表单2(使用相同的调用约定)兼容。许多旧的标准。库函数是这样的(因为它们被认为是),因为它们在那里形成C的早期版本,其中没有函数声明,并且它们都是在表单2中。其他函数可能无意地与表单2兼容,如果它们的参数为在此表单的参数提升规则中声明。但有些可能并非如此。

但是表单2需要程序员无处不在地传递相同类型的参数,因为编译器不能用原型检查参数并且不得不确定调用约定的实际传递参数。

例如,在MC68000机前两个整数参数为固定ARG功能(这两种形式1和2)将在寄存器D0D1,第一两个指针在A0A1传递,所有其他人通过堆栈传递。因此,例如功能fwrite(const void * ptr, size_t size, size_t count, FILE * stream);将得到的参数为:ptrA0sizeD0countD1A1stream(在D0返回结果)。当你包括stdio.h它会是如此,无论你传递给它。

当你不包括stdio.h另一件事情发生。当你用fwrite(data, sizeof(*data), 5, myfile)调用fwrite时,编译器在argruments上查找并看到该函数被调用为fwrite(*, int, int, *)。那么它做了什么?它传递第一个指针A0,第一个int在D0,第二个int在D1和第二个指针在A1,所以它是我们所需要的。

但是,当您尝试将其称为fwrite(data, sizeof(*data), 5.0, myfile)时,与count是双重类型,编译器将尝试通过count通过堆栈,因为它不是整数。但功能要求在D1。狗屎发生:D1包含一些垃圾,而不是count,所以进一步的行为是不可预知的。但是,如果使用在stdio.h中定义的原型,那么所有将会正常:编译器会自动将此参数转换为int并根据需要传递它。这不是抽象的例子,因为双重结构可能只是涉及浮点数的计算结果,你可能会错过这个假设结果是int。

另一个例子是可变参数函数(形式3),如printf(char *fmt, ...)。因为它的调用约定需要最后一个命名参数(这里是fmt)通过它的类型的堆栈管理。所以,你打电话printf("%d", 10)它会把指针指向"%d"和号码10堆栈和调用函数根据需要。

但是,当你不包括stdio.h comiler不会知道printf是可变参数的函数,将假设printf("%d", 10)呼吁与类型的指针和INT的固定参数的功能。因此MC68000会将指针指向A0,并将int指向D0而不是堆栈,并且结果再次不可预知。

可能有运气,参数先前在堆栈中,偶尔会在那里读取,并且您会得到正确的结果...这一次...但是另一次会失败。另一个幸运的是,如果没有声明的函数可能是可变参数(并且以某种方式使调用与两种形式兼容),编译器会小心。或者所有形式的所有参数都只是通过机器上的堆栈传递,因此固定,未知和可变参数形式只是被称为相同的。

所以:不要这样做,即使你感到幸运,它的工作原理。未知的固定参数表单仅用于与旧代码兼容,并且严格不鼓励使用。

另请注意:C++根本不会允许,因为它要求函数用已知参数声明。

相关问题