2010-07-09 78 views
2

我不正确理解C++程序的编译和链接。有没有办法,我可以看看通过编译C++程序(以可理解的格式)生成的目标文件。这应该有助于我理解目标文件的格式,如何编译C++类,编译器需要什么信息来生成目标文件,并帮助我理解如下语句:需要帮助了解C++程序的编译

如果一个类只用作输入参数和返回类型,我们不需要包含整个类头文件。前向声明就足够了,但是如果派生类从基类派生,我们需要包含包含基类定义的文件(取自“Exceptional C++”)。

我正在阅读“链接和加载”一书来理解目标文件的格式,但我更喜欢专门为C++源代码量身打造的东西。

感谢,

Jagrati

编辑:

我知道,与纳米我可以看看目前在目标文件的符号,但我想知道更多有关目标文件。

+4

我不认为看对象文件将有助于理解你提到的观点。思考“编译器需要知道如何为这个输入创建机器代码”这个问题可能更有用?例如,要将'A * a'传递给下一个函数,编译器不需要知道A是什么样子,而是调用'a-> foo()',它就是这样。从“A”派生,至少需要知道“A”的大小和所有方法签名。 – 2010-07-09 07:46:04

+0

嗨克里斯托弗,我同意你的观点。事实上,这就是我想从哪里开始。但是,即使像“从A派生出来的东西,至少需要知道A的大小”对我来说也不是那么明显。为什么不能将A的大小知识推迟到说出运行时间,或者说在链接时间,而不是在创建目标文件时的编译时间。因此,我得出结论,我可能需要从不同的角度理解编译器在目标文件中放置什么信息。 – xyz 2010-07-09 07:54:45

+1

我认为Stanley B Lippman的书:“C++内部对象模型”可以帮助你理解一些主题 – 2010-07-09 08:50:04

回答

0

你有没有试过用readelf(假设你在Linux平台上)检查你的二进制文件?这提供了有关ELF对象文件的非常全面的信息。老实说,虽然我不确定这对理解编译和链接有多大帮助,但是我并不确定这会有多大帮助。我认为正确的方法可能是掌握C++代码如何映射到程序集的前后链接。

0

您通常不需要详细了解Obj文件的内部格式,因为它们是为您生成的。所有你需要知道的是,对于你创建的每一个类,编译器都会生成Obj文件,它是你的类的二进制字节码,适合于你编译的操作系统。然后,下一步 - 链接 - 将您的程序所需的所有类的目标文件放在一个EXE或DLL中(或其他任何非Windows OS-es的格式)。也可以是EXE +几个DLL,这取决于你的意愿。

最重要的是你分开你的类的接口(声明)和实现(定义)。

总是只放在你的类的头文件接口声明。没有别的 - 这里没有实现。避免使用自定义类型的成员变量,这些类型不是指针,因为对于它们来说,前向声明是不够的,你需要在头文件中包含其他头文件。如果你的标题包含了,那么设计就会有气味,并且会减慢构建过程。

类方法或其他函数的所有实现应该在CPP文件中。这将保证当有人包含您的头文件时,编译器生成的Obj文件将不再需要,并且您只能从CPP文件中包含其他文件。

但为什么要麻烦?答案是,如果你有这样的分隔,那么链接速度会更快,因为每个类都使用了每个Obj文件。另外,如果你改变了你的类,这将在下一次构建时改变其他一些对象文件。

如果你在头文件中包含了,这意味着当编译器为你的类生成Obj文件时,它应该首先为你的头文件中包含的其他类生成Obj文件,这可能需要其他Obj文件等等。甚至可能是循环依赖,然后你不能编译!或者如果你改变了你的类中的某些东西,那么编译器将需要重新生成许多其他的Obj文件,因为如果你没有分离,它们会在一段时间后变得非常紧密依赖。

+0

RE:“避免使用自定义类型也是成员变量” - 如何避免使用原始指针的W/O?我猜想会是聪明的指针,但任何其他的想法? – msi 2010-07-09 07:55:53

+0

@msiemeri:无论如何,我认为建议是夸大了。您可能希望在少数情况下这样做来打破依赖周期,但这与一般建议不同。是的,在这种情况下,应该使用scoped_ptr或类似的。 – peterchen 2010-07-09 08:26:07

0

nm是一个unix工具,它将显示对象文件中符号的名称。

objdump是一个GNU工具,它会告诉你更多的信息。

但是,这两种工具都会显示链接器使用的非常原始的信息,但不是为人类设计的。这可能不会帮助你更好地理解在C++级别发生的事情。

1

第一件事,第一件事。反汇编编译器输出很可能不会以任何方式帮助您理解您的任何问题。编译器的输出不再是一个C++程序,而是简单的汇编,如果你不知道内存模型是什么,那么阅读起来就非常棘手。

论为什么是base所需的定义,当你宣称它是一个基类的derived有几个不同的原因(也可能更多,我忘了)的特殊问题:

  1. 当创建derived类型的对象,编译器必须保留内存为全实例和所有子类:它必须知道的base
  2. 大小当你访问一个成员属性的编译器必须知道从隐含this指针偏移量,该抵消需要知道第th所采用的大小e base子对象。
  3. 当在derived的上下文中分析标识符并且在derived类中找不到标识符时,编译器必须在查找封闭名称空间中的标识符之前知道它是否在base中定义。如果在base类中声明foo(),编译器无法知道foo();derived::function()内是否为有效呼叫。
  4. 当编译器定义derived类时,必须知道在base中定义的所有虚函数的编号和签名。它需要这些信息来建立动态调度机制 - 通常是vtable--,甚至要知道derived中的成员函数是否被绑定为动态调度 - 如果base::f()是虚拟的,那么derived::f()将是虚拟的,不管是否为derived中的声明具有virtual关键字。
  5. 多重继承增加了一些其他要求 - 比如每个baseX必须在调用方法的最终重写器之前重写的相对偏移量(类型为base2的指针指向multiplyderived的对象并不指向实例,但对base2子对象的情况下,这可能是由继承列表base2之前宣布的其他基地可以抵消年初

最后一个问题的意见:

因此,对象(除了全局对象)的实例化可以等到运行时,因此大小和偏移等可以等到链接时间,并且在生成目标文件时我们不一定要处理它。

void f() { 
    derived d; 
    //... 
} 

前面的代码分配和在堆栈derived类型的对象。编译器将添加汇编指令以为堆栈中的对象保留一定量的内存。编译器解析并生成程序集后,没有对象的踪迹,特别是(假设POD类型的一个简单的构造函数:即没有被初始化),该代码和void f() { char array[ sizeof(derived) ]; }将生成完全相同的汇编程序。当编译器生成将保留空间的指令时,它需要知道多少。