【问题标题】:C Preprocessor directives and LinkingC 预处理器指令和链接
【发布时间】:2013-11-10 01:12:23
【问题描述】:

#include 指令将导致在编译之前将头文件的内容放在源代码中:例如,如果我包含 stdio.h ,则预处理器会将 stdio.h 的所有内容放在源代码中然后编译不是吗?

所以让我们假设我只是在我的代码中使用 printf() 函数。所以在编译之后和链接之间肯定会发生一些事情,这将删除头文件中包含的所有函数实现,并且只在可执行文件的导入表中插入 printf() 函数实现,知道所有其他函数是用源码编译的。请你给我解释一下好吗?

【问题讨论】:

  • 其他功能不是删了。如果您不调用函数,则不会生成引用它的代码,因此由于从未创建过链接而丢失了链接。这就像说“电话公司给我发了电话簿。我查找了我的朋友并打电话给他。当我收到电话账单时,我看到账单上只列出了我的朋友。谁删除了所有我没有打电话的电话号码?”
  • @RaymondChen,不完全是。链接时优化将在每个对象文件的基础上消除未使用的代码。至于你的例子,电话账单实际上可能包含所有家庭成员的记录。
  • @liuyu 这里的问题不是链接器清理。没有什么需要清理的,因为它从未被创建。
  • 头文件中(通常)没有函数调用——只有函数(以及结构、常量等)的签名(返回类型、参数类型和函数名称)的声明。
  • @RaymondChen,您假设链接到共享库,其中可执行文件仅包含对函数实现的引用。但这个问题最好用静态链接来解释,即链接器如何处理在<stdio.h> 中声明但未在程序中调用的函数。

标签: c++ c


【解决方案1】:

在 C 头文件中声明的 函数原型 充当编译时约束。它识别正确数量的参数和这些参数的正确类型。此类约束的验证发生在编译阶段(即*.c -> *.o)。

(静态)链接阶段以每个对象文件为基础从编程库中消除未使用的二进制对象。编程库是二进制对象文件 (*.o) 的存档,每个文件都包含一组函数、常量等的实现。至于您的问题,二进制对象文件包含 printf 的实现(以及其他所有内容)在同一个目标文件中)将链接到您的程序中。其他未使用的目标文件将作为链接时优化而被淘汰。

【讨论】:

  • 消除通常按节执行,而不是按目标文件执行。可以通过使用 __attribute__((section(...))) 之类的东西来利用这一点
  • @anatolyg,我同意链接在技术上是每个部分的。但由于-ffunction-section 不是gcc 中的默认选项(在编译编程库时不是默认选项),因此节的粒度通常超出单个函数实现。因此,我们通常不能期望在不引入冗余的情况下链接被调用函数,至少在桌面计算的情况下不会。
【解决方案2】:

printf 函数实际上并不在头文件中,而是在链接器自动链接到可执行文件的库中。

头文件应该只包含函数原型,即函数的声明而不是定义。由于头文件中没有函数定义,因此实际上不会为它们生成任何代码,并且编译器将确保只有您实际调用的函数才能在生成的目标文件中获得一个条目,以便链接器知道它。

【讨论】:

  • 所以链接器的任务是包含这些函数的代码以生成可执行权限?
  • @JohnnyCat 是的。它获取编译器生成的所有目标文件和所有库,并将它们组合成一个可执行文件。
  • 头文件可以包含函数定义。
  • @MirceaIonica 虽然在技术上是正确的(它们也可以包含变量定义),但它是链接器错误的根源。稍微改写了我的答案。
  • 现在我看到你已经编辑了“应该”,这使我的评论过时了。附言您可以拥有可以在头文件中定义的内联函数。
【解决方案3】:

头文件只包含一个声明,例如形式

size_t printf(char const *format, ...);

这告诉编译器如果遇到printf这个词并且被用作函数,那么它可以生成函数调用。

调用指令包含函数实际地址的占位符,然后在构建最终可执行文件时由链接器插入。

链接器通常只保留一个尚未解析的符号的运行列表,并在遇到这些占位符之一时添加到此列表中,并在找到定义时填写实际地址 为符号。在printf 的情况下,该定义位于 C 标准库中,因此链接器确保库在运行时加载,并且调用指令指向正确的地址。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-05
    • 1970-01-01
    相关资源
    最近更新 更多