声明头文件必不可少?这段代码:
main()
{
int i=100;
printf("%d\n",i);
}
似乎工作,我得到的输出是100.即使不使用stdio.h
头文件。这怎么可能?
答案 0 :(得分: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’
答案 1 :(得分:9)
这怎么可能?简而言之:三件运气。
这是可能的,因为一些编译器会对未声明的函数做出假设。具体而言,假设参数为int
,返回类型也为int
。由于int
通常与char*
大小相同(取决于体系结构),因此您可以通过传递int
和字符串,因为正确的大小参数将被推送到堆栈。
在您的示例中,由于未声明printf
,因此假设它采用了两个int
参数,并且您传递了char*
和int
“兼容“就调用而言。所以编译器耸了耸肩并生成了一些应该是正确的代码。 (它确实应该警告你有关未声明的功能。)
所以第一个好运是编译器的假设与实际函数兼容。
然后在链接器阶段,因为printf
是C标准库的一部分,编译器/链接器将自动在链接阶段包含它。由于printf
符号确实在C stdlib中,因此链接器解析了符号并且一切都很好。链接是第二块运气,因为除了标准库之外的任何地方都需要链接它的库。
最后,在运行时我们会看到你的第三块运气。编译器做了一个盲目的假设,该符号碰巧默认链接在一起。但是 - 在运行时,您可以轻松传递数据,从而导致应用程序崩溃。幸运的是,参数匹配,并且正确的事情最终发生了。这肯定并非总是如此,我敢说上面的内容可能在64位系统上失败了。
所以 - 回答原始问题,包含头文件真的很重要,因为如果它有效,那只能通过盲目的运气!
答案 2 :(得分:0)
正如paxidiablo所说,它不是必需的,但这仅适用于函数和变量,但如果您的头文件提供了您使用的某些类型或宏(#define),那么您必须包含头文件才能使用它们,因为它们之前是必需的链接发生,即在预处理或编译期间
答案 3 :(得分:0)
这是可能的,因为当C编译器看到未声明的函数调用(在您的情况下为printf())时,它假定它具有
int printf(...)
签名并尝试调用它将所有参数转换为int类型。由于“int”和“void *”类型通常具有相同的大小,因此大部分时间都可以使用。但依靠这种行为是不明智的。
答案 4 :(得分:0)
C支持三种类型的函数参数形式:
foo(int x, double y)
。foo()
(不要与foo(void)
混淆:它是第一个没有参数的形式),或者根本没有声明。foo(int x, ...)
。当您看到标准函数工作时,函数定义(形式为1或3)与表单2兼容(使用相同的调用约定)。很多老std。库函数是如此(因为它们被认为是),因为它们存在于C的早期版本中,其中没有函数声明,它们都是形式2.其他函数可能无意中与表单2兼容,如果它们的参数为在此表单的参数提升规则中声明。但有些可能不是这样。
但是表单2需要程序员在任何地方传递相同类型的参数,因为编译器无法使用原型检查参数并且必须确定调用约定实际传递的参数。
例如,在MC68000机器上,固定arg函数的前两个整数参数(对于表单1和2)将在寄存器D0
和D1
中传递,{{1}中的前两个指针}和A0
,所有其他人都通过堆栈。因此,例如,函数A1
将获取参数:fwrite(const void * ptr, size_t size, size_t count, FILE * stream);
中的ptr
,A0
中的size
,D0
中的count
和D1
中的stream
(并在A1
中返回结果)。当你包括D0
时,无论你传递给它,都会如此。
当你不包括stdio.h
时,会发生另一件事。当您使用stdio.h
编译器调用fwrite查看argruments并查看该函数被称为fwrite(data, sizeof(*data), 5, myfile)
。那么它做什么?它传递fwrite(*, int, int, *)
中的第一个指针,A0
中的第一个int,D0
中的第二个int和D1
中的第二个指针,这是我们需要的。
但是当你尝试将其称为A1
时,fwrite(data, sizeof(*data), 5.0, myfile)
是双重类型,编译器将尝试通过堆栈传递count
,因为它不是整数。但是函数require在count
中。发生了什么:D1
包含一些垃圾,而不是D1
,所以进一步的行为是不可预测的。但是,与使用count
中定义的原型相比,一切都可以:编译器自动将此参数转换为int并根据需要传递它。这不是抽象的例子,因为arument中的double可能只是涉及浮点数的计算结果,你可能会错过这个假设结果是int。
另一个例子是变量参数函数(表单3),如stdio.h
。因为调用约定要求最后命名的参数(此处为printf(char *fmt, ...)
)通过其类型的堆栈regardess传递。那么,你调用fmt
它会在堆栈上放置指针printf("%d", 10)
和数字"%d"
并根据需要调用函数。
但是如果你不包括10
,那么comiler就不会知道stdio.h
是vararg函数,并且假设printf
正在调用 fixed 参数类型指针和int。所以MC68000会将指向printf("%d", 10)
和int的指针放到A0
而不是堆栈,结果再次变得无法预测。
可能幸运的是,参数先前已经堆叠,偶尔会在那里读取并且您得到正确的结果......这次......但是另一个时间将会失败。另一个好运是编译器注意,如果没有声明的函数可能是vararg(并且以某种方式使调用与两种形式兼容)。或者所有形式的所有参数都只是通过机器上的堆栈传递,因此固定的,未知的和vararg形式只是被称为相同。
所以:即使你觉得自己很幸运,也不要这样做。未知的固定参数形式只是为了与旧代码兼容,严格禁止使用。
另请注意:D0
根本不允许这样做,因为要求函数使用已知参数声明。