需要帮助理解C ++程序的编译

时间:2010-07-09 07:25:23

标签: c++ linker compilation object-files

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

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

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

谢谢,

Jagrati

编辑:

我知道使用nm我可以查看目标文件中存在的符号,但我有兴趣了解有关目标文件的更多信息。

5 个答案:

答案 0 :(得分:1)

首先,首先。反汇编编译器输出很可能无法以任何方式帮助您理解您遇到的任何问题。编译器的输出不再是一个c ++程序,而是简单的程序集,如果你不知道内存模型是什么,那就非常难以阅读。

关于为什么在声明它是base的基类时需要定义derived的特定问题,有几个不同的原因(可能还有更多我忘了):

  1. 当创建类型为derived的对象时,编译器必须为完整实例和所有子类保留内存:它必须知道base的大小
  2. 当您访问成员属性时,编译器必须知道隐式this指针的偏移量,并且该偏移量需要知道base子对象所采用的大小。
  3. 当在derived的上下文中解析标识符并且在derived类中找不到标识符时,编译器必须在查找标识符之前知道它是否在base中定义在封闭的命名空间中。如果在foo();类中声明derived::function(),则编译器无法知道foo()base内的有效调用。
  4. 当编译器定义base类时,必须知道derived中定义的所有虚函数的数量和签名。它需要这些信息来构建动态调度机制 - 通常是vtable--,甚至知道derived中的成员函数是否绑定动态调度 - 如果base::f()是虚拟的,那么无论derived::f()中的声明是否包含derived关键字,virtual都将是虚拟的。
  5. 多重继承增加了一些其他要求 - 比如每个baseX的相对偏移量,必须在调用方法的最终重写器之前重写(类型为base2的指针指向一个对象multiplyderived不指向实例的开头,而是指向实例中base2子对象的开头,这可能会被继承列表中base2之前声明的其他基数所抵消。
  6. 评论中的最后一个问题:

      

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

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

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

答案 1 :(得分:0)

您是否尝试使用readelf检查二进制文件(假设您在Linux平台上)?这提供了有关ELF目标文件的非常全面的信息。

但老实说,我不确定这对理解编译和链接有多大帮助。我认为正确的方法可能是处理C ++代码如何映射到装配前后链接。

答案 2 :(得分:0)

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

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

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

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

但为什么要这么麻烦?答案是,如果你有这样的分离,那么链接更快,因为你的每个Obj文件每个类使用一次。此外,如果您更改了类,这将在下一次构建期间更改少量其他目标文件。

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

答案 3 :(得分:0)

nm是一个unix工具,它会显示目标文件中符号的名称。

objdump是一个GNU工具,可以向您显示更多信息。

但是这两个工具都会向您展示链接器使用的非常原始的信息,但不是为人类阅读而设计的。这可能无法帮助您更好地理解在C ++级别发生的事情。

答案 4 :(得分:0)

我正在阅读“http://www.network-theory.co.uk/docs/gccintro/” - “GCC简介”。这使我在链接和编译方面有了很好的洞察力。它在初学者级别,但我不在乎。