为什么编译器只从.cpp文件生成目标文件.o

Mek*_*nis 1 compiler-construction linker header-files object-files

正如标题所示:为什么编译器仅从 .cpp 文件而不是头文件生成目标文件 .o ?如果实现位于 .h 文件中,链接器如何知道如何将目标文件链接在一起?

Mik*_*han 5

为什么编译器只从.cpp文件而不是头文件生成目标文件.o

具体而言,我假设编译器是 GCC 的 C++ 编译器。

如果您明确表示这就是您真正想要的,编译器会将头文件编译为目标文件。

头文件.h

#ifndef HEADER_H
#define HEADER_H

#include <iostream>

inline void hw()
{
    std::cout << "Hello World" << std::endl;
}

#endif
Run Code Online (Sandbox Code Playgroud)

如果你只是这样做:

$ g++ header.h
Run Code Online (Sandbox Code Playgroud)

那么它不会生成目标文件,因为它从.h 扩展中假设您不希望它生成。相反,它会生成一个预编译头文件header.h.gch.

这是一个合理的假设,因为通常我们不想将头文件直接编译为目标文件。通常,我们根本不想直接编译头文件,如果要的话,我们想要的是预编译头文件。

但如果你确实想header.h编译为header.o,你可以坚持这样:

$ g++ -c -x c++ header.h
Run Code Online (Sandbox Code Playgroud)

其中表示:编译,不链接,header.h将其视为 C++ 源文件。输出是header.o.

然而,这header.o毫无用处。例如,它不会将单独的函数导出hw到链接器,因为该函数是内联的。如果我们查看目标文件中的符号:

$ objdump -C -t header.o

header.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 header.h
0000000000000000 l    d  .text  0000000000000000 .text
0000000000000000 l    d  .data  0000000000000000 .data
0000000000000000 l    d  .bss   0000000000000000 .bss
0000000000000000 l     O .bss   0000000000000001 std::__ioinit
0000000000000000 l     F .text  000000000000003e __static_initialization_and_destruction_0(int, int)
000000000000003e l     F .text  0000000000000015 _GLOBAL__sub_I_header.h
0000000000000000 l    d  .init_array    0000000000000000 .init_array
0000000000000000 l    d  .note.GNU-stack    0000000000000000 .note.GNU-stack
0000000000000000 l    d  .eh_frame  0000000000000000 .eh_frame
0000000000000000 l    d  .comment   0000000000000000 .comment
0000000000000000         *UND*  0000000000000000 std::ios_base::Init::Init()
0000000000000000         *UND*  0000000000000000 .hidden __dso_handle
0000000000000000         *UND*  0000000000000000 std::ios_base::Init::~Init()
0000000000000000         *UND*  0000000000000000 __cxa_atexit
Run Code Online (Sandbox Code Playgroud)

我们看到那里除了样板文件和 . 拉入的东西之外什么都没有#include <iostream>

我们可以header.h通过删除关键字来使链接可用inline。然后,如果我们像以前一样重新编译并再看一下:

$ objdump -C -t header.o | grep hw
0000000000000061 l     F .text  0000000000000015 _GLOBAL__sub_I__Z2hwv
0000000000000000 g     F .text  0000000000000023 hw() 
Run Code Online (Sandbox Code Playgroud)

我们已经出口了hw()!我们可以header.o在程序中链接!

主程序

extern void hw();

int main()
{
    hw();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

编译一下:

$ g++ -c main.cpp
Run Code Online (Sandbox Code Playgroud)

关联:

$ g++ -o prog main.o header.o
Run Code Online (Sandbox Code Playgroud)

跑步:

$ ./prog
Hello World
Run Code Online (Sandbox Code Playgroud)

但有一个障碍。现在我们已经定义了hw()inheader.h以便链接器可以看到它,我们不能 header.h 再以通常使用头文件的方式使用它,即我们不能#include "header.h"在一个.cpp文件中同时编译和链接多个文件。相同的程序:

main1.cpp

extern void foo();
extern void bar();

int main()
{
  foo();
  bar();
  return 0;
}
Run Code Online (Sandbox Code Playgroud)

foo.cpp

#include "header.h"

void foo(){
    hw();
};
Run Code Online (Sandbox Code Playgroud)

酒吧.cpp

#include "header.h"

void bar(){
    hw();
};
Run Code Online (Sandbox Code Playgroud)

将它们全部编译:

$ g++ -c main1.cpp foo.cpp bar.cpp
Run Code Online (Sandbox Code Playgroud)

都好。所以链接:

% g++ -o prog main1.o foo.o bar.o
bar.o: In function `hw()':
bar.cpp:(.text+0x0): multiple definition of `hw()'
foo.o:foo.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
Run Code Online (Sandbox Code Playgroud)

不好,因为hw()被定义了两次,一次在 中,foo.o 又一次在 中bar.o,这是一个链接错误:链接器无法选择一个定义而不是另一个定义。

因此,如果您坚持的话,您会看到编译器愿意并且能够将.h 文件编译为 C++ 源文件;如果您坚持的话,它能够并且愿意将.blahblah文件编译为 C++ 源代码,假设文件中存在合法的 C++ .blahblah。但是编译为目标文件的头文件对我们来说几乎没有用处。

.h文件和文件之间的区别.cpp只是我们打算如何使用文件的常规区别。如果我们给出.h扩展名,我们就是说:该文件中的所有 C++ 都可以安全地包含在.cpp编译和链接在一起的多个翻译单元(文件)中。如果我们给它一个.cpp扩展名,我们就是说:至少这个文件中的一些 C++ 只能在同一链接中编译和链接一次。

根据这个约定,header.h我们开始使用的一个正确的头文件。根据此约定,header.h我们从中删除的不再inline是头文件。如果我们不喜欢让人们感到困惑的话,我们应该将其重命名为。.cpp

如果实现位于 .h 文件中,链接器如何知道如何将目标文件链接在一起

链接器只链接目标文件和库。它不知道有关.cpp文件或.h文件的任何信息:就链接器而言,它们可能不存在。头文件中的“实现”可以通过三种方式到达链接器。

1)我们刚才讨论的非常规方式:将头文件编译为目标文件,这是链接的。正如您所看到的,这样做不存在技术 问题,尽管实际上从未这样做过。

2)通常的方式,通过在文件#include中添加头文件.cpp

你好.h

#ifndef HELLO_H
#define HELLO_H

static char const * hw = "Hello world";

#endif
Run Code Online (Sandbox Code Playgroud)

你好.cpp

#include "hello.h"
char const * hello = hw;
Run Code Online (Sandbox Code Playgroud)

在这种情况下,编译器 甚至在开始生成目标代码之前就进行预处理,并且您可以通过告诉编译器执行预处理而不执行其他操作来查看编译器在预处理器完成后看到的内容: hello.cpp

$ g++ -P -E hello.cpp
static char const * hw = "Hello world";
char const * hello = hw;
Run Code Online (Sandbox Code Playgroud)

该命令的输出是将编译为 的 翻译单元hello.o,如您所见,中的代码hello.h只是复制到翻译单元中的 位置#include "hello.h"

因此,当编译器开始生成 时hello.o,标头hello.h 已经无关紧要:它可能不存在。

3)通过将header.h文件编译成预编译的header.h.gch. 这 header.h.gch是一种“半编译”形式,如果存在,无论何时或出现在代码中,都header.h将被-ed。唯一的区别是半编译的处理速度比:(3)只是(2)的更快版本(并且它有一个限制,即编译器每次编译只接受一个预编译头。)#include#include "header.h"#include <header.h>header.h.gchheader.h

无论是通过(1)(2)还是(3)到达那里,文件中代码的链接.h与文件中代码的链接没有什么不同.cpp。所有代码均由编译器编译。编译器不关心代码是否源自.h文件或.cpp文件。编译器生成目标文件,链接器链接目标文件。