隐式头包含在C中

时间:2012-04-02 14:31:09

标签: c compilation linker include

  

program.c

int main () {

    hello();
    return 0;
}
  

tools.c

void hello (void) {

    printf("hello world\n");
}
  

Makefile

program : program.o tools.o

在这个程序的文件集中,我没有 tools.h 文件,即使编译好没有错误,有人可以解释C程序中头文件的用途吗?

现在我只有一个想法:编译层需要结构等变量......

但在我的情况下,如果头文件只包含函数原型,是否需要更长时间来构建它? (makefile链接器语法更容易捕获)。

6 个答案:

答案 0 :(得分:1)

头文件通常包含源c文件中定义的函数的声明。

它的目的是什么?

  • 它为您提供额外的安全性,编译器会根据声明检查传递给函数的参数,并在发现错误时报告错误。
  • 它们允许从实现中分离接口。基本上,这允许您将代码(实现)作为库提供,客户端需要链接而只需在其应用程序中包含接口头文件。

答案 1 :(得分:1)

在C中,您可以调用一个尚未声明的函数,它被假定为一个返回int的extern函数,编译器将允许您传递任意类型的任意数量的参数。 请注意,不建议这样做。

头文件是向C编译器通知正确签名,特别是返回类型所必需的。如果存在声明,编译器将检查传递的参数是否与声明匹配,尽管在非常旧的样式C中,仅检查参数的数量,而不是它们的类型。由于你不能在C中重载函数,我相信它是有效的(虽然不推荐,并且它可能在运行时实际上不起作用,具体取决于所使用的调用约定)来声明一个函数:

int hello();

然后实际将其实现为:

int hello(char* who) {
    printf("Hello %s\n", who);
}

链接器会将这些内容链接在一起。请注意,这不是好风格。

请注意,这是C ++中的关键更改之一,您必须在调用函数之前声明函数,并检查所有参数的类型。

答案 2 :(得分:0)

它们允许您在不同的c文件之间共享函数,变量,结构的声明。

答案 3 :(得分:0)

在您的示例中,您将两者一起编译。所以没关系,尽管缺少“hello”的前向声明,但你没有收到任何错误。

但使用头文件有很多优点,例如:   轻松链接多个文件中的符号,避免每个源文件中的前向声明等。

这大大避免了源文件中的多个声明,最重要的是减少编译时间只是告诉编译器定义在代码中的某处

答案 4 :(得分:0)

在调用时没有范围内hello()的原型,编译器假设(正确配置时发出警告)原型为int hello()(注意, int hello(void))。

但该定义与该原型不一致:void hello(void) vs int hello()所以你刚刚发布了未定义的行为。任何事情都可能发生。具体来说,您的程序可以按照您的预期编译和运行。

您可以通过提供正确的原型(通过编写和包含头文件)或直接在program.c源文件中指定原型来避免 UB

答案 5 :(得分:0)

来自GCC docs

  

头文件有两个目的。

     
      
  • 系统头文件声明了操作系统各部分的接口。您可以将它们包含在程序中,以提供调用系统调用和库所需的定义和声明。
  •   
  • 您自己的头文件包含程序源文件之间接口的声明。每次有一组相关的声明和宏定义时,在几个不同的源文件中都需要所有或大部分声明,最好为它们创建一个头文件。
  •   
     

包含头文件会产生与将头文件复制到需要它的每个源文件相同的结果。这种复制将是耗时且容易出错的。使用头文件,相关声明只出现在一个地方。如果需要更改它们,可以在一个地方更改它们,包含头文件的程序将在下次重新编译时自动使用新版本。头文件消除了查找和更改所有副本的工作量以及无法找到一个副本将导致程序内部不一致的风险。

我认为C标题是C早期的遗留文物。

重要的是要理解C预处理器将标题逐字包含在源文件中。所以问题真的变成了:为什么我们需要前瞻性声明?

  • 为了保持较小的可执行文件,类型信息不会嵌入C编译器生成的目标代码中。因此,链接目标文件或库需要定义每个函数提供的类型,因为此信息来自目标文件不可用。现代编译器通过检查源代码或库的定义来解决这个问题 - 标识符和签名直接从源代码或库符号中获取。

  • 为了使编译器简单有效,从它们将链接的库中键入检查定义是不合适的。实际上,编译一个文件是完全有效的,即使在编译机上也没有使用过的库。类似地,在编译依赖项之前延迟编译类型是很繁琐的,而在循环关系的情况下则完全不可能。这要求在使用函数之前使函数签名可用(用于类型检查)。为方便起见,C默认为int fun(...),以便在最常见的情况下,减少前向声明函数的需要。

为了让每个人的生活更轻松,手动转发声明被委托给预处理器。实际上,C编译器没有头文件的概念。相反,声明按逻辑组织成头文件,然后由预处理器添加到程序预编译中。

这使程序员不必在每个编译单元的开头键入所有需要的声明,但它实际上就是发生的事情。

所有这些扭曲实际上都是C早期存在的局限性的结果。

然而,有一些明显的优势会导致这些扭曲。在实现代码本身之外提供函数定义允许在接口和实现之间进行明确分离,这实际上允许构建更清晰的系统。在理想的世界中,您只需要头文件来使用库,而不需要先验证实现。 (除此之外:如果你找到一只粉红色的独角兽,我会为你换一个理想的世界。

现代语言有更高级别的构造来分隔实现形式接口:Java中的interfaces,Python中的duck typing,Clojure中的Protocols,Eiffel中的contracts等。