4

我了解 C 程序(堆栈、堆、函数调用等)的内存是如何组织的。现在,我真的不明白所有这些东西在面向对象语言(更具体地说,C++)中是如何工作的。

我知道每当我使用new关键字时,对象的空间都会分配到堆上。

我对此的一些基本问题是:

1)在程序执行期间类定义是否存储在内存中的某处?

2)如果是,那么它存储在哪里以及如何存储。如果不是,那么在运行时如何调度函数(在虚拟/非虚拟函数的情况下)。

3)当一个对象被分配内存时,关于该对象的所有细节都存储在其中吗?(它属于哪个类,成员函数,公共私有变量/函数等)

所以基本上,有人可以解释一下面向对象的代码是如何在编译之后/期间转换的,以便实现这些 OOP 功能吗?

我对 Java/C++ 很满意。因此,您可以使用任何一种语言来解释逻辑,因为它们都具有截然不同的特征。

另外,请添加任何参考链接,以便我也可以从那里阅读,以防万一出现进一步的疑问!

谢谢!

4

4 回答 4

9

1)在程序执行期间类定义是否存储在内存中的某处?

在 C++ 中,没有。在 Java 中,是的。

2)如果是,那么它存储在哪里以及如何存储。如果不是,那么在运行时如何调度函数(在虚拟/非虚拟函数的情况下)。

在 C++ 中,对非虚函数的调用被编译器替换为函数的实际静态地址;对虚拟函数的调用通过虚拟表进行。new被转换为内存分配(编译器知道精确的大小),然后调用(静态确定的)构造函数。编译器将字段访问转换为访问对象开头的静态已知偏移量的内存。

它在 Java 中很相似——特别是,虚拟表用于虚拟调用——除了字段访问可以象征性地完成。

3)当一个对象被分配内存时,关于该对象的所有细节都存储在其中吗?(它属于哪个类,成员函数,公共私有变量/函数等)

在 C++ 中 - 不存储元数据(嗯,除了RTTI所需的一些位)。在 Java 中,您可以获得所有成员的类型信息和可见性以及其他一些信息 - 您可以查看Java 类文件定义以获取更多信息。

所以基本上,有人可以解释一下面向对象的代码是如何在编译之后/期间转换的,以便实现这些 OOP 功能吗?

从我上面的答案中可以看出,这实际上取决于语言。

在像 C++ 这样的语言中,繁重的工作由编译器完成,生成的代码与面向对象的概念几乎没有关系——事实上,C++ 编译器的典型目标语言(本机二进制代码)是无类型的。

在像 Java 这样的语言中,编译器以中间表示为目标,该中间表示通常包含许多额外的细节——类型信息、成员可见性等。这也是在这些语言中启用反射的原因。

于 2013-10-15T13:31:54.350 回答
3

在程序执行期间,类定义是否存储在内存中的某处?

定义不会被保留——至少不是在维护编译时所拥有的信息的意义上。

当一个对象被分配内存时,关于该对象的所有详细信息都存储在其中吗?(它属于哪个类,成员函数,公共私有变量/函数等)

在编译期间,诸如对字段的引用之类的东西被转换为具有固定偏移量的指针的取消引用。例如,thea->first可能被翻译为*(a + 4), a->secondas*(a + 8)等。实际数字将取决于先前字段的大小、目标架构等。

类似的事情适用于对象的大小(用于分配和释放)。

简而言之,对象的大小及其字段的偏移量在编译时是已知的,并且它们在实际的二进制文件中被替换

如果不是,那么在运行时如何调度函数(在虚拟/非虚拟函数的情况下)。

像虚拟方法调用这样的东西通常以与字段类似的方式翻译,因为它们也可以被视为该类的隐藏数据结构(称为vtable )的“字段”。指向给定类的 vtable 的指针存储在(该类的)每个对象中,如果它具有虚拟方法。

非虚拟方法的正确实现在编译时是已知的,因此这些方法可以在现场“链接”而无需使用 vtable。

于 2013-10-15T13:37:39.947 回答
2

细节可能有所不同,但通常对于我们拥有的每个 C++ 类:

  • 它的一组方法,每个方法只是一个函数,并且
  • 虚拟方法表:一个数组,其中每个元素都引用此类或其超类之一的方法

没有虚方法的对象只是一个类似于 C 中的结构。一旦声明了虚方法,对象就会获得一个引用虚拟表的隐藏字段(下图vmt)。

非虚拟方法obj.m(arg)的调用被转换为类 C 函数的调用,m$(obj, arg)其中m$是由 C++ 编译器生成的一些人工标识符,以区分m命名方法与其他类中的命名方法。

虚方法的调用obj.m(arg)转换为(obj->vmt[N])(obj, arg),即从对象的虚表中取出实际函数。每种方法在表中都有自己的编号。这个数字在编译时是已知的,并被硬编码到调用指令序列中。

没有其他信息在运行时保存/用于普通执行。可以保留更多信息以用于调试目的。

于 2013-10-15T13:34:07.660 回答
0

查看 C++ 标准以了解应该与所有编译器共享哪些指令。C++ 标准规定了对象在内存中的布局方式的一些细节。这些限制应该在编译器之间共享。但是,细节留给语言的实现。以下是我发现的超出标准的常见特征。

一个没有继承或静态字段的简单对象的布局就像您看到的那样。C++ 要求内存是字节寻址的,但这并不意味着数据将与字节对齐。它将符合编译器的规范(取决于架构和其他因素)。大多数情况下,我发现数据与单词对齐。如果你发现它是按字打包的,而且你只有一个字节,那么内存中字节之间就会有空白点。如果需要,除了对虚函数表的引用之外,没有对象的元数据。当你谈到继承和多重继承时,它变得更加复杂。

函数与对象分开存储,并且如何转换对象决定了您将调用哪些函数,这些函数将对象视为他们期望的对象。这一切都有效,因为实际上,该函数有一个隐藏的 this 指针作为它的第一个参数。没有运行时检查来确保您引用了正确的对象类型。如果您将一个对象转换为另一个对象并在其上调用函数,则该函数可能会遇到内存异常。c 风格的演员表没有类型安全,避免它们。

然后你有一个虚函数表,它根据你正在访问的类型返回指向函数的指针。但同样,这一切都是在编译时决定的。

当您使用具有反射的语言时,这种情况会发生巨大变化。

存储类型元数据以供运行时使用,并且在运行时进行类型检查。在错误的类型上调用错误的方法会导致异常。

于 2013-10-15T13:43:22.373 回答