【问题标题】:C header files and compilation/linkingC 头文件和编译/链接
【发布时间】:2013-09-04 02:00:09
【问题描述】:

我知道头文件有在“调用”#include.c 文件中使用的各种函数、结构等的前向声明,对吧?据我了解,“三权分立”是这样发生的:

头文件:func.h

  • 包含函数的前向声明

    int func(int i);
    

C源文件:func.c

  • 包含实际的函数定义

    #include "func.h"
    
    int func(int i) {
        return ++i ;
    }
    

C 源文件source.c(“实际”程序):

#include <stdio.h>
#include "func.h"

int main(void) {
    int res = func(3);
    printf("%i", res);
}

我的问题是:看到#include 只是一个编译器指令,它将.h 的内容复制到#include 所在的文件中,.c 文件如何知道如何实际执行功能?它得到的只是int func(int i);,那么它如何实际执行该功能呢?它如何访问func 的实际定义?标题是否包含某种“指针”,上面写着“那是我的定义,在那边!”?

它是如何工作的?

【问题讨论】:

  • 这就是Linker 解析定义并确保您在编译期间声称存在的东西确实存在的魔力。
  • 在处理头文件时,您可能想了解一下include guards
  • 我知道包含警卫(ifndef 就是这样),但为简洁起见省略了它们。
  • 对我来说,您似乎认为 源代码 被用于执行程序,例如在脚本语言(JavaScript 等)中。事实并非如此。 C 源代码首先(由编译器和链接器)转换为机器代码,然后由处理器硬件直接执行。
  • 你是什么意思,“源代码被用来执行程序”? C(或 GCC 实现)是编译的,而不是解释的。我知道它是提前编译成机器码的。它是用于生成机器代码的源代码。我不确定你的意思是什么。

标签: c linker


【解决方案1】:

鼬内亚给出了答案。它是链接器

使用 GNU C 编译器 gcc 你会编译一个像

这样的单文件程序
gcc hello.c -o hello # generating the executable hello

但如示例中所述编译两个(或更多)文件程序,您必须执行以下操作:

gcc -c func.c # generates the object file func.o
gcc -c main.c # generates the object file main.o
gcc func.o main.o -o main # generates the executable main

每个目标文件都有外部符号(您可以将其视为公共成员)。函数默认是外部的,而(全局)变量默认是内部的。您可以通过定义来改变这种行为

static int func(int i) { # static linkage
    return ++i ;
}

/* global variable accessible from other modules (object files) */
extern int global_variable = 10; 

当遇到对未在主模块中定义的函数的调用时,链接器会搜索所有目标文件(和库)作为定义被调用函数的模块的输入。默认情况下,您可能有一些库链接到您的程序,这就是您可以使用printf 的方式,它已经编译成一个库。

如果您真的有兴趣,请尝试一些汇编编程。这些名称相当于汇编代码中的标签。

【讨论】:

  • 所以使用 GCC 的模式是: 1. 对每个 .c(带有定义)和 .h(带有 func 原型)使用 -c 标志来创建每个 .o 2. 使用 -o 标志和每个 .o 文件来创建最终的 exe ?
  • 是的。 “-c”选项用于“编译”,因此只是将目标代码编译成目标文件。没有 -c 的 gcc 识别输入是目标文件,因此它只是使用链接器将它们链接在一起。最后,-o 标志是可选的,它用于指定可执行文件的输出文件名。
【解决方案2】:

它是处理所有这些的链接器。编译器只是在目标文件中发出一个特殊序列,为链接器说“我有这个外部符号func,请解决它”。然后链接器看到它,并在所有其他目标文件和库中搜索该符号。

【讨论】:

  • 这是否意味着项目中的所有.c文件都会被搜索到?
  • @LidongGuo 如果您在命令行上将所有源文件一起编译,或者如果您从所有源创建目标文件并将它们全部链接在一起,那么是的,它们将被搜索。但它不是自动完成的,您必须告诉链接器您要链接哪些目标文件,并且只会搜索那些。
  • @Someprogrammerdude 但是链接器如何从 .o 文件中知道哪些函数在哪个标头下导出以及哪些函数在哪个标头下未解析,是头文件名还是存储在符号表,以便链接器确保它匹配(即标头在两个文件中,并且只有一个目标文件导出)
  • @LewisKelsey 头文件不参与链接。一个目标文件基本上是一个单独的translation unit,以编译的形式。链接器知道符号在哪里定义,因为目标文件也包含在翻译单元中定义的符号。如果您想了解有关链接器如何工作的更多详细信息,我建议您在您最喜欢的搜索引擎中搜索相关信息,因为这里需要回答的主题非常广泛(尤其是在 cmets 中)。
【解决方案3】:

在同一编译单元中没有定义的符号声明告诉编译器将该符号地址的占位符编译到目标文件中。

链接器将看到符号的定义是必需的,并将在库和其他目标文件中查找符号的外部定义。

如果链接器找到定义,则原始目标文件中的占位符将替换为最终可执行文件中找到的地址。

【讨论】:

    【解决方案4】:

    标头不仅可以访问同一程序中的其他.c 文件,还可以访问可能以二进制形式分发的库。一个.c 文件与另一个文件的关系与依赖于另一个文件的库完全相同。

    由于无论实现的格式如何,编程接口都需要采用文本形式,因此头文件作为关注点的分离是有意义的。

    正如其他人所提到的,解析函数调用以及库和源(翻译单元)之间的访问的程序称为链接器。

    链接器不适用于标头。它只是制作了一个包含所有翻译单元和库中定义的所有名称的大表,然后将这些名称链接到访问它们的代码行。 C 的古老用法甚至允许在没有任何实现声明的情况下调用函数;只是假设每个未定义的类型都是int

    【讨论】:

      【解决方案5】:

      一般当你像这样编译一个文件时:

      gcc -o program program.c
      

      您确实是在调用一个驱动程序,它执行以下操作:

      • 使用cpp 进行预处理(如果您要求它是一个单独的步骤)。
      • 使用cc1编译(可能与预处理集成)
      • 组装,使用as(gas,GNU 汇编器)。
      • 使用collect2 进行链接,它也使用ld(GNU 链接器)。

      通常,在前 3 个阶段,您会创建一个简单的目标文件(.o 扩展名),该文件是通过编译一个编译单元(即 .c 文件,#include 和其他指令替换为预处理器)。

      第四阶段是创建最终可执行文件的阶段。编译完一个单元后,编译器会将几段代码标记为需要由链接器解析的引用。链接器的工作是在许多编译单元中搜索并解析对外部编译单元的引用。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-10-26
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多