2011-05-05 89 views
42

我已经详细阅读了可能的重复,但是没有一个答案有下沉C中的头文件和源文件如何工作?

TL;博士:在C如何相关的源文件和头文件?项目在构建时隐式地清理声明/定义依赖项吗?

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

鉴于这些文件:

header.h

int returnSeven(void); 

由source.c

int returnSeven(void){ 
    return 7; 
} 

的main.c

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

这混乱会编译?我目前正在做我的工作,来自Cygwin的NetBeans 7.0gcc,它可以自动执行大部分构建任务。在编译项目时,涉及的项目文件将根据header.h中的声明,明确列出source.c

+1

是的,这将汇编(为什么你认为这是一个“烂摊子“?)。要学习的概念是**编译单元**和**链接**。 – Jesper 2011-05-05 21:56:09

+0

谢谢** Jesper **;哈哈,这不是一团糟,我想这个词最适合描述我的大脑,可以在3本初学C级书籍之间阅读。我肯定会研究*编译单元*和*链接*,但为了专注于学习语法,我会让** NetBeans ** + ** gcc **为我解决这个问题。鉴于这种情况,只要给定的头文件在项目的其他地方存在定义的声明,那么包含该头文件足以提供对定义的功能的访问,编译器会对这些细节进行整理? – Dan 2011-05-05 22:00:39

+1

'header.h'需要包括守卫;) – alternative 2011-05-05 22:01:22

回答

58

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

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

在第一步骤中,编译器需要编译单元,这通常是一个预处理的源文件(所以,所有的标题的内容的源文件,它#include多个)并将其转换为一个对象文件。

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

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

您的功能returnSevensource.c中定义(并且main功能在main.c中定义)。总而言之,您有两个编译单元:source.cmain.c(包含它包含的头文件)。您将它们编译为两个目标文件:source.omain.o。第一个将包含returnSeven的定义,第二个为main的定义。然后链接程序会将这两个文件粘贴在一个可执行程序中。

关于联动:

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

+0

谢谢** Jesper **;你的回答几乎涉及到我所困惑的所有问题。综合回应 – Dan 2011-05-05 23:19:30

+0

4年之后,我在回顾这个问题,我有点害怕,虽然这个答案写得很好,内容翔实,但几乎完全没有回答实际问题,它几乎没有提到头文件 – 2015-11-09 19:28:26

+1

几乎完全不能回答实际问题。它几乎没有提到头文件。“这是解释整个编译过程的最佳答案之一。 – 2016-08-04 12:54:02

23

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

你的例子应该编译(尽管有愚蠢的语法错误)。例如,使用GCC,您可能会首先执行:

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

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

您然后执行:

gcc -o my_prog source.o main.o 

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

+1

(注意:这与main.c必须能够看到returnSeven()的声明这一事实截然不同:我很迂腐,但这不完全正确。编译器会很高兴地编译(with在C99中有一个警告),并且链接器解析它,通常会导致不好的结果。例如,文件ac中的 ,调用'x = bob(1,2,3,4)'和文件bc中的'void bob(char * a){}'将编译,链接并运行。 – mattnz 2011-05-05 23:49:42

+0

绝对世界级的答案。爱极简主义的GCC编译器指令示例 – 2017-07-18 13:58:12

2

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

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

4

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

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

10

编译没有什么魔力。也不自动!

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

考虑“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该文件更为方便。包含你在输入原型时不会冒犯错误的风险。

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


当他们长大甚至更多,你把所有的已编译的代码库中......这就是另一个故事:)