【问题标题】:C/C++ header and implementation files: How do they work?C/C++ 头文件和实现文件:它们是如何工作的?
【发布时间】:2012-03-02 17:32:27
【问题描述】:

这是可能一个愚蠢的问题,但我已经在这里和网上搜索了很长一段时间,但无法找到明确的答案(我的尽职调查谷歌搜索)。

所以我是编程新手...我的问题是,主函数如何知道不同文件中的函数定义(实现)?

例如。假设我有 3 个文件

  • main.cpp
  • myfunction.cpp
  • myfunction.hpp

//main.cpp

#include "myfunction.hpp"
int main() {
  int A = myfunction( 12 );
  ...
}

-

//myfunction.cpp

#include "myfunction.hpp"
int myfunction( int x ) {
  return x * x;
}

-

//myfunction.hpp

int myfunction( int x );

-

我知道预处理器是如何包含头代码的,但是头和主函数是如何知道函数定义存在的,更不用说利用它了?

如果这不是很清楚,或者我对某些事情有很大的误解,我深表歉意,这里是新的

【问题讨论】:

标签: c++ compilation header-files


【解决方案1】:

1.原理

当你写作时:

int A = myfunction(12);

这被翻译成:

int A = @call(myfunction, 12);

其中@call 可以看作是字典查找。如果您考虑字典类比,您当然可以在知道它的定义之前知道一个词 (smogashboard ?)。您所需要的只是,在运行时,定义在字典中。

2.关于 ABI 的一点

这个 @call 是如何工作的?因为 ABI。 ABI 是一种描述许多事物的方式,其中包括如何执行对给定函数的调用(取决于其参数)。调用合约很简单:它只是说明可以找到每个函数参数的位置(有些在处理器的寄存器中,有些在堆栈中)。

因此,@call 实际上是这样做的:

@push 12, reg0
@invoke myfunction

并且函数定义知道它的第一个参数(x)位于reg0

3.但我认为字典是用于动态语言的?

在某种程度上,你是对的。动态语言通常使用散列表来实现,用于动态填充的符号查找。

对于 C++,编译器会将翻译单元(粗略地说,预处理的源文件)转换为对象(一般为.o.obj)。每个对象都包含一个它引用但定义未知的符号表:

.undefined
[0]: myfunction

然后链接器会将对象组合在一起并协调符号。此时有两种符号:

  • 在库中,可以通过偏移量引用(最终地址仍未知)
  • 那些在库之外,其地址在运行时之前完全未知。

两者都可以用同样的方式处理。

.dynamic
[0]: myfunction at <undefined-address>

然后代码将引用查找条目:

@invoke .dynamic[0]

当库被加载时(例如DLL_Open),运行时最终会知道符号在内存中的映射位置,并用真实地址覆盖&lt;undefined-address&gt;(对于本次运行)。

【讨论】:

    【解决方案2】:

    您必须了解,从用户的角度来看,编译是一个两步操作。


    第一步:对象编译

    在此步骤中,您的 *.c 文件被单独编译成单独的目标文件。这意味着当main.cpp 被编译时,它对你的myfunction.cpp 一无所知。他唯一知道的是您声明具有此签名的函数:int myfunction( int x ) 存在于另一个目标文件中。

    编译器将保留此调用的引用并将其直接包含在目标文件中。目标文件将包含一个“我必须用 an int 调用 myfunction,它会用 an int 返回给我。它保留一个索引所有extern 电话,以便之后能够与其他人联系。


    第二步:链接

    在此步骤中,linker 将查看目标文件的所有索引,并尝试解决这些文件中的依赖关系。如果没有,你会得到著名的undefined symbol XXX。然后,他会将这些引用转换为结果文件中的实际内存地址:二进制文件或库。


    然后,您可以开始问,如何使用像 Office 套件这样具有大量方法和对象的巨大程序来做到这一点?好吧,他们使用shared library 机制。您可以通过 Unix/Windows 工作站上的“.dll”和/或“.so”文件了解它们。它允许将未定义符号的求解推迟到程序运行为止。

    它甚至可以通过dl* 函数按需解决未定义的符号。

    【讨论】:

      【解决方案3】:

      当您编译程序时,预处理器会将每个头文件的源代码添加到包含它的文件中。编译器编译 EVERY .cpp 文件。结果是多个.obj 文件。
      之后是链接器。链接器获取所有.obj 文件,从您的主文件开始,每当它找到没有定义的引用(例如变量、函数或类)时,它会尝试在编译阶段创建的其他.obj 文件中找到相应的定义或在链接阶段开始时提供给链接器。
      现在回答您的问题:每个.cpp 文件都被编译成一个包含机器代码指令的.obj 文件。当您包含.hpp 文件并使用在另一个.cpp 文件中定义的某个函数时,在链接阶段,链接器会在相应的.obj 文件中查找该函数定义。它就是这样找到它的。

      【讨论】:

        【解决方案4】:

        预处理器将头文件的内容包含在cpp文件中(cpp文件称为翻译单元)。 编译代码时,会分别检查每个翻译单元的语义和句法错误。不考虑跨翻译单元的函数定义的存在。编译后生成.obj文件。

        在下一步链接 obj 文件时。搜索使用的函数(类的成员函数)的定义并进行链接。如果未找到该函数,则会引发链接器错误。

        在您的示例中,如果函数未在 myfunction.cpp 中定义,编译仍会继续进行,没有问题。链接步骤会报错。

        【讨论】:

          【解决方案5】:

          正如 Matthieu M. 的评论中所建议的,链接器工作是在正确的位置找到正确的“功能”。编译步骤大致是:

          1. 为每个 cpp 文件调用编译器并将其转换为 带有符号表的目标文件(二进制代码) 函数名称(名称在 C++ 中被修改)到它们在 目标文件。
          2. 链接器只被调用一次:其中的每个目标文件 范围。它将从一个对象解析函数调用位置 感谢符号表。一个 main() 函数必须 存在于某处。最终生成一个二进制可执行文件 当链接器找到它需要的一切时。

          【讨论】:

            【解决方案6】:

            头文件声明函数/类 - 即在编译 .cpp 文件时告诉编译器哪些函数/类可用。

            .cpp 文件定义了这些函数 - 即编译器编译代码并因此生成实际的机器代码来执行相应.hpp 文件中声明的那些操作。

            在您的示例中,main.cpp 包含一个 .hpp 文件。预处理器将#include 替换为.hpp 文件的内容。该文件告诉编译器函数myfunction在别处定义,它接受一个参数(int)并返回int

            因此,当您将main.cpp 编译成目标文件(.o 扩展名)时,它会在该文件中注明它需要函数myfunction。当您将myfunction.cpp 编译成一个目标文件时,该目标文件中有一个注释,它具有myfunction 的定义。

            然后,当您将两个目标文件链接到一个可执行文件中时,链接器会将结果绑定在一起 - 即 main.o 使用 myfunction,如 myfunction.o 中定义的那样。

            希望对你有帮助

            【讨论】:

              【解决方案7】:

              int myfunction(int); 是函数原型。你用它声明函数,这样编译器就知道你在写myfunction(0);时调用了这个函数。

              header 和 main 函数怎么知道函数定义存在?
              嗯,这是Linker的工作。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2011-08-19
                • 2011-08-22
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2010-11-29
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多