如果我不包含头文件会发生什么?

4
如果在运行C程序时不包含头文件,会发生什么情况呢?我知道会出现警告,但程序可以正常运行。我知道头文件包含函数声明,那么当我不包含它们时,编译器如何找到这些声明呢?它会检查所有的头文件吗?

5个回答

7
我知道我会收到警告,但程序运行得非常好。
这是ANSI C前不幸的遗留问题:语言不需要函数原型,所以标准C至今仍允许(通常可以通过警告来查找调用无原型函数)。
当您调用没有原型的函数时,C编译器对被调用的函数做出以下假设:
- 假定函数的返回类型为int - 所有参数都已声明(即没有... vararg stuff) - 所有参数都假定为默认提升后传递的内容等。
如果调用无原型函数符合这些假设,则您的程序将正确运行;否则,它是未定义的行为。

如果我在没有包含头文件的情况下调用printf函数,如果编译器假定上述内容,那么printf如何能够完美地工作? - DesirePRG
@DesirePRG 大多数现代编译器都了解标准 C 函数,例如 printf。它们会超越对来自 C 标准的函数进行常规参数检查 - I/O、内存管理等。例如,某些编译器会在您将 float 传递给 printf 时发出警告,而格式字符串相应位置为 %d。但是,并非所有编译器都会这样做,也没有任何编译器需要这样做。 - Sergey Kalinichenko
@dasblinkenlight:至少在gcc的情况下,编译器不会使用该知识生成隐式转换以使代码“正常工作”,例如将float转换为int用于"%d"。它只是用它来生成一个警告。 printf("%d\n",1.25)仍然会产生荒谬的结果。 - Keith Thompson
@KeithThompson 是的,完全正确。我遇到了600个字符的限制,所以我没有详细说明第二部分。不过还是谢谢你的提醒! - Sergey Kalinichenko
参数不被假定为 int,它们被假定为与您传递的内容匹配(在参数提升之后)。因此,如果您在没有包含 #include <math.h> 的情况下调用 sqrt(1.0),编译器将假定 int sqrt(double) -- 当然这仍然是错误的。这是 C90 的规定;C99 取消了 "隐式 int" 规则。 - Keith Thompson
C99也删除了隐式函数声明。因此,虽然您不一定需要原型,但在使用函数时,必须在作用域中具有声明(且调用必须匹配声明)。 - Daniel Fischer

5
在1989年的ANSI C标准之前,没有办法声明一个函数并指定它的参数类型。你只需要非常小心地使每个函数调用与被调用的函数一致,如果出错(比如将int传递给sqrt()),编译器不会发出警告。在没有可见的声明的情况下,任何你调用的函数都被认为返回int,这就是“隐式int”规则。许多标准函数确实返回int,所以通常可以省略#include
1989年的ANSI C标准(实质上也是1990年的ISO C标准)引入了原型,但并未强制要求其使用(现在仍然是如此)。因此,如果你调用函数,你可能需要注意遵循该函数的约定或者添加自己的原型声明。
int c = getchar();

实际上这会起作用,因为getchar()返回一个int

1999年的ISO C标准取消了隐式int规则,并使在没有可见声明的情况下调用函数成为非法(实际上是一种约束违反)。因此,如果您调用标准函数而没有必需的#include,符合C99的编译器必须发出诊断(可能只是警告)。非原型函数声明(不指定参数类型的声明)仍然是合法的,但被视为过时。

(2011年的ISO C标准在这个特定领域没有多少变化。)
但仍有大量的代码是为C90编译器编写的,大多数现代编译器仍支持旧标准。

所以,如果您在没有必需的#include的情况下调用标准函数,可能会发生以下情况:(a)编译器将警告您缺少声明,(b)它将假设函数返回int并接受您实际传递的任何数量和类型的参数(还考虑类型提升,如shortintfloatdouble)。如果调用是正确的,并且如果您的编译器宽容,那么您的代码可能会正常工作 - 但是如果它因某种无关原因失败,则您将有更多要担心的事情。

printf这样的变参函数是另一回事。即使在C89/C90中,使用没有可见原型调用printf也具有未定义的行为。编译器可以针对可变参数函数使用完全不同的调用约定,因此printf("hello")puts("hello")可能会生成完全不同的代码。但是,出于与旧代码的兼容性考虑,大多数编译器使用兼容的调用约定,因此例如K&R1中的第一个“hello world”程序可能仍然可以编译和运行。

您还可以为标准函数编写自己的声明;编译器不关心它是否在标准头文件中看到声明还是在您自己的源文件中看到。但是这样做没有意义。声明从一个标准版本到下一个标准版本 subtle地发生了变化,您的实现附带的头文件应该是正确的。

那么,如果您调用一个标准函数而没有相应的#include,会发生什么呢?
在典型的工作环境中,这并不重要,因为如果运气好的话,您的程序将无法通过代码审查。

原则上,符合C99或更高版本的任何编译器都可能以致命错误消息拒绝您的程序。(gcc将使用-std=c99 -pedantic-errors这种方式)实际上,大多数编译器只会打印警告。如果函数返回int(或者如果您忽略结果),并且您正确获取了所有参数类型,则该调用可能有效。如果调用不正确,则编译器可能无法打印良好的诊断信息。如果函数不返回int,编译器可能会假定它会返回int,从而导致您获得垃圾结果,甚至使程序崩溃。

因此,您可以研究我的答案,接着阅读各个版本的C标准,找出您的编译器符合哪个版本的标准,并确定在什么情况下可以安全地省略#include头文件 - 但风险是下次修改程序时可能会出错。

或者,您可以注意编译器的警告(使用可用的任何命令行选项启用),阅读每个调用的函数的文档,在每个源文件的顶部添加所需的#include,并且不必担心任何这些问题。


1
首先:只需包含它们。
如果不这样做,编译器将使用未声明函数的默认原型,其为:
int functionName(int argument);

如果函数可用,它将编译和链接。但是在运行时会出现问题。


0

为了与旧程序兼容,C编译器可以编译调用未声明函数的代码,假设参数和返回值的类型为int。会发生什么?例如,看看这个问题:Troubling converting string to long long in C我认为这是一个很好的例子,说明如果您不包括必要的头文件并因此未声明使用的函数,您可能会遇到的问题。那位用户遇到的问题是他尝试使用atoll而没有包括声明atollstdlib.h文件:

char s[30] = { "115" };
long long t = atoll(s);
printf("Value is: %lld\n", t);

令人惊讶的是,这打印出了0,而不是预期的115!为什么?因为编译器没有看到atoll的声明,并假定它的返回值是一个int,因此只选择了函数在堆栈上留下的部分值,换句话说,返回值被截断了。

这就是为什么建议使用-Wall(所有警告)编译代码的原因之一。


0

如果省略头文件,你将无法做很多事情:

(我希望从评论中得到更多信息,因为我的记忆正在衰退...)

  • 你无法使用头文件中定义的任何宏。这可能非常重要。
  • 编译器无法检查你是否正确调用函数,因为头文件为其定义了参数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接