我理解如何为C程序(堆栈,堆,函数调用等)组织内存。 现在,我真的不明白所有这些东西在面向对象语言中是如何工作的(更具体地说,是C ++)。
我知道每当我使用 new 关键字时,对象的空间就会分配到堆上。
我的一些基本问题是:
1)在程序执行期间,类定义是否存储在内存中?
2)如果是,则存储位置和方式。如果不是,那么在运行时如何调度函数(如果是虚拟/非虚函数)。
3)当一个对象被分配内存时,该对象的所有细节都存储在其中? (它属于哪个类,成员函数,公共私有变量/函数等。)
基本上,有人可以解释在编译之后/期间如何转换面向对象的代码,以便这些O.O.P.功能是否实现?
我对Java / C ++很满意。因此,您可以使用其中一种语言解释逻辑,因为两者都具有非常明显的特征。
此外,请添加任何参考链接,以便我也可以从那里阅读,以防万一有进一步的疑问!
谢谢!
答案 0 :(得分:9)
1)在程序执行期间,类定义是否存储在内存中?
在C ++中,没有。在Java中,是的。
2)如果是,则存储在何处以及如何存储。如果不是,那么在运行时如何调度函数(如果是虚拟/非虚函数)。
在C ++中,对非虚函数的调用被编译器替换为函数的实际静态地址;通过virtual table调用虚拟函数。 new
被转换为内存分配(编译器知道精确的大小),然后调用(静态确定的)构造函数。编译器将字段访问转换为访问距离对象开头的静态已知偏移量的内存。
在Java中类似 - 特别是,虚拟表用于虚拟调用 - 除了可以象征性地进行字段访问。
3)当一个对象被分配内存时,该对象的所有细节都存储在其中? (它属于哪个类,成员函数,公共私有变量/函数等。)
在C ++中 - 没有存储元数据(除了RTTI所需的一些位之外)。在Java中,您可以获得所有成员的类型信息和可见性以及其他一些内容 - 您可以查看Java class file definition以获取更多信息。
基本上,有人可以解释在编译之后/期间如何转换面向对象的代码,以便这些O.O.P.功能是否实现?
从上面的答案可以看出,这实际上取决于语言。
在像C ++这样的语言中,繁重的工作由编译器完成,结果代码与面向对象的概念几乎没有关系 - 实际上,C ++编译器的典型目标语言(本机二进制代码)是无类型的
在像Java这样的语言中,编译器以中间表示为目标,该表示通常包含许多额外的细节 - 类型信息,成员可见性等。这也是在这些语言中启用reflection的原因。
答案 1 :(得分:3)
在执行程序期间,类定义是否存储在内存中?
不保留定义 - 至少不是在编译时维护您拥有的信息。
当一个对象被分配内存时,该对象的所有细节都存储在其中? (它属于哪个类,成员函数,公共私有变量/函数等。)
在编译期间,诸如字段引用之类的内容将转换为具有固定偏移量的指针的解引用。例如,a->first
可能会被翻译为*(a + 4)
,a->second
等*(a + 8)
等等。实际数字取决于先前字段的大小,目标架构等。
类似的东西适用于对象的大小(出于分配和释放的目的)。
简而言之, 的对象及其字段的偏移在编译时是已知的,而在实际二进制文件中被替换
如果不是,那么在运行时如何调度函数(如果是虚拟/非虚函数)。
虚拟方法调用之类的东西通常以与字段类似的方式进行翻译,因为它们也可以被视为该类的隐藏数据结构(称为 vtable )的“字段”。如果具有虚拟方法,则指向给定类的vtable的指针存储在(该类的)每个对象中。
非虚拟方法的正确实现在编译时是已知的,因此这些方法可以在不使用vtable的情况下在现场“链接”。
答案 2 :(得分:2)
细节可能有所不同,但通常对于我们拥有的每个C ++类:
没有虚方法的对象只是C中的结构。只要声明了虚方法,对象就会获得一个隐藏字段,该字段引用虚拟表(下面是vmt
)。
将非虚方法obj.m(arg)
的调用转换为类C函数m$(obj, arg)
的调用,其中m$
是由C ++编译器生成的一些人工标识符,用于区分名为{{1}的方法来自其他类中的命名方法。
虚拟方法m
的调用转换为obj.m(arg)
,即实际函数取自对象的虚拟表。每个方法在表中都有自己的编号。这个数字在编译时是已知的,并且硬编码到调用指令序列中。
在运行时没有为普通执行保存/使用其他信息。可以保留更多信息以用于调试目的。
答案 3 :(得分:0)
查看C ++标准,了解应与所有编译器共享的任务。 C ++标准管理有关如何在内存中布置对象的一些细节。这些限制应该在编译器之间共享。但是,细节留给了语言的实现。以下是我发现的超出标准的特征。
没有继承或静态字段的简单对象就像你看到的那样。 C ++要求内存是字节寻址的,但这并不意味着数据将与字节对齐。它将与编译器的规范一致(取决于架构和其他因素)。大多数情况下,我发现数据与单词对齐。如果你发现它是按单词打包的,而你只有单个字节,那么内存在字节之间会有空白点。如果需要,除了对虚拟功能表的引用之外,没有对象的元数据。当你进行继承和多重继承时,它变得更加复杂。
函数与对象分开存储,以及如何转换对象决定了您将调用对象的函数,这些函数看起来就像它们期望的那样。这一切都有效,因为实际上,函数有一个隐藏的this指针作为它的第一个参数。没有运行时检查以确保您引用正确的对象类型。如果将对象转换为另一个对象并在其上调用函数,则该函数可能会遇到内存异常。 c型演员没有类型安全,避免它们。
然后你有了虚函数表,它根据你访问的类型返回指向函数的指针。但同样,这一切都是在编译时决定的。
当你到达一个有反射的语言时,这会发生巨大的变化。
存储类型元数据以供运行时使用,并且在运行时进行类型检查。在错误的类型上调用错误的方法会有异常。