【问题标题】:How do header and source files in C work?C 中的头文件和源文件如何工作?
【发布时间】:2011-08-19 18:11:51
【问题描述】:

我已经仔细阅读了可能的重复项,但是没有一个答案会陷入其中。

tl;dr:C 中的源文件和头文件是如何关联的?项目是否在构建时隐式整理声明/定义依赖关系?

我试图了解编译器如何理解.c.h 文件之间的关系。

鉴于这些文件:

header.h

int returnSeven(void);

source.c

int returnSeven(void){
    return 7;
}

ma​​in.c

#include <stdio.h>
#include <stdlib.h>
#include "header.h"
int main(void){
    printf("%d", returnSeven());
    return 0;
}

这个烂摊子会编译吗?我目前正在使用来自 Cygwin 的 gccNetBeans 7.0 中进行工作,它可以自动执行大部分构建任务。编译项目时,所涉及的项目文件是否会根据header.h 中的声明来整理source.c 的隐含包含?

【问题讨论】:

  • 是的,这将编译(为什么你认为这是一个“混乱”?)。要了解的概念是编译单元链接
  • 谢谢Jesper;哈哈,这不是乱七八糟的,我想这个词最好用来描述我的大脑,在 3 个初级水平之间阅读C 书籍。我肯定会研究编译单元链接,但是为了专注于学习语法,我会让NetBeans + gcc 帮我解决这个问题。鉴于此,只要给定的头文件具有在项目中其他地方存在定义的声明,包含该头文件就足以提供对已定义功能的访问,并且编译器会整理出细节?
  • header.h 需要包括警卫;)
  • 另外我建议手动编译。 gcc main.c -c -o main.ogcc source.c -c -o source.ogcc main.o source.o -o program 将编译它。它可以很容易地看到单独的编译单元和最后的链接。

标签: c compilation header-files


【解决方案1】:

编译没有什么神奇的。也不是自动的!

头文件基本上向编译器提供信息,几乎从不提供代码。
仅凭这些信息通常不足以创建完整的程序。

考虑“hello world”程序(使用更简单的puts 函数):

#include <stdio.h>
int main(void) {
    puts("Hello, World!");
    return 0;
}

没有标头,编译器不知道如何处理puts()(它不是C 关键字)。标头让编译器知道如何管理参数和返回值。

然而,这个简单的代码中没有指定函数的工作原理。其他人为puts() 编写了代码,并将编译后的代码包含在库中。作为编译过程的一部分,该库中的代码包含在源代码的编译代码中。

现在考虑您想要自己的puts() 版本

int main(void) {
    myputs("Hello, World!");
    return 0;
}

只编译这段代码会出错,因为编译器没有关于函数的信息。您可以提供该信息

int myputs(const char *line);
int main(void) {
    myputs("Hello, World!");
    return 0;
}

并且代码现在可以编译——但没有链接,即没有生成可执行文件,因为没有myputs() 的代码。所以你将myputs() 的代码写在一个名为“myputs.c”的文件中

#include <stdio.h>
int myputs(const char *line) {
    while (*line) putchar(*line++);
    return 0;
}

并且您必须记住将您的第一个源文件和“myputs.c”一起编译

一段时间后,您的“myputs.c”文件已扩展为充满功能的手,您需要在要使用它们的源文件中包含有关所有功能(它们的原型)的信息。
将所有原型写入单个文件和#include 该文件更方便。包含在内,您在键入原型时不会有犯错的风险。

你仍然需要编译和链接所有的代码文件。


当它们增长得更多时,你将所有已经编译的代码放在一个库中......这是另一回事 :)

【讨论】:

    【解决方案2】:

    将 C 源代码文件转换为可执行程序通常分两步完成:编译链接

    首先,编译器将源代码转换为目标文件 (*.o)。然后,链接器获取这些目标文件以及静态链接的库并创建一个可执行程序。

    在第一步中,编译器采用一个编译单元,它通常是一个预处理的源文件(因此,一个包含它#includes 的所有头文件内容的源文件)和将其转换为目标文件。

    在每个编译单元中,所有用到的函数都必须声明,让编译器知道函数存在以及它的参数是什么。在您的示例中,函数returnSeven 的声明位于头文件header.h 中。编译 main.c 时,在声明中包含标头,以便编译器在编译 main.c 时知道 returnSeven 存在。

    当链接器完成它的工作时,它需要找到每个函数的定义。每个函数必须在一个目标文件中只定义一次 - 如果有多个目标文件包含相同函数的定义,则链接器将停止并出现错误。

    您的函数returnSevensource.c 中定义(而main 函数在main.c 中定义)。

    因此,总而言之,您有两个编译单元:source.cmain.c(以及它包含的头文件)。您将它们编译为两个目标文件:source.omain.o。第一个包含returnSeven 的定义,第二个包含main 的定义。然后链接器会将这两者粘合到一个可执行程序中。

    关于联动:

    外部链接内部链接。默认情况下,函数具有外部链接,这意味着编译器使这些函数对链接器可见。如果你创建一个函数static,它有内部链接——它只在定义它的编译单元内可见(链接器不会知道它存在)。这对于在源文件内部执行某些操作并且您希望对程序的其余部分隐藏的函数很有用。

    【讨论】:

      【解决方案3】:

      C 语言没有源文件和头文件的概念(编译器也没有)。这只是一个约定;请记住,头文件始终是#included 到源文件中;在正确编译开始之前,预处理器实际上只是复制粘贴内容。

      您的示例应该编译(尽管有愚蠢的语法错误)。例如,使用 GCC,您可能首先会这样做:

      gcc -c -o source.o source.c
      gcc -c -o main.o main.c
      

      这会分别编译每个源文件,创建独立的目标文件。在这个阶段,returnSeven()main.c内部还没有被解析;编译器只是以某种方式标记了目标文件,表明它必须在将来被解析。所以在这个阶段,main.c 看不到returnSeven()定义 不是问题。 (注意:这与main.c 必须能够看到returnSeven()声明 才能编译这一事实不同;它必须知道它确实是一个函数,以及它的原型是什么是的。这就是为什么你必须在main.c#include "source.h"。)

      然后你做:

      gcc -o my_prog source.o main.o
      

      链接两个目标文件到一个可执行的二进制文件中,并执行符号解析。在我们的示例中,这是可能的,因为main.o 需要returnSeven(),而这由source.o 公开。如果所有内容都不匹配,则会导致链接器错误。

      【讨论】:

      • (注意:这与 main.c 必须能够看到 returnSeven() 的声明这一事实不同:我很迂腐,但这并不完全正确。编译器会很高兴地编译(在 C99 中带有警告)此代码和链接器解析它,通常会产生不良影响。例如,在文件 ac 中,调用 x=bob(1,2,3,4) 在文件 bc 中,void bob(char *a){} 将编译、链接和运行。
      • 绝对世界级的答案。喜欢极简的 GCC 编译器指令示例
      【解决方案4】:

      头文件用于分隔与源文件中的实现相对应的接口声明。他们以其他方式被滥用,但这是常见的情况。这不是针对编译器的,而是针对编写代码的人的。

      大多数编译器实际上并不单独查看这两个文件,它们是由预处理器组合在一起的。

      【讨论】:

        【解决方案5】:

        编译器本身对源文件和头文件之间的关系没有特定的“知识”。这些类型的关系通常由项目文件(例如,makefile、解决方案等)定义。

        给定的示例看起来好像可以正确编译。您需要编译这两个源文件,然后链接器需要这两个目标文件来生成可执行文件。

        【讨论】:

          猜你喜欢
          • 2012-03-02
          • 1970-01-01
          • 1970-01-01
          • 2017-06-29
          • 2016-01-11
          • 1970-01-01
          • 1970-01-01
          • 2010-10-08
          相关资源
          最近更新 更多