表示内存中属性和方法的对象

发布于 2025-01-13 18:10:46 字数 54 浏览 3 评论 0原文

在内存中表示属性和方法的对象,是否有人有图片或绘图来解释计算机如何处理它并将属性存储在内存中?

Representing objects of properties and methods in memory , if anyone have picture or drawing to expalin how computer deal with it and store properties in memory?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

笔芯 2025-01-20 18:10:46

计算机实际上并不在基础层面上存储此类抽象信息。在那里,你基本上有数字——二进制的,但这并不重要——通常由软件来解释这些数字。

冯·诺伊曼模型中,几乎每个系统都基于这个模型,你有一个大地址空间。您可以对其进行索引,这样您的 CPU 就可以获取给定地址上的数字,或者将新数字写入某个地址,这就是存储数据的主要内容。通常(但并非总是),地址会选择内存中的各个字节,但您的计算机可以寻址到更大或更小的字大小,例如,您可能拥有一台可以寻址到 32 位字而不是 8 位字的计算机。不过,这对于整体模型来说并不重要。您只需拥有一大块内存,就可以在各个地址获取数据。

如何解释这些数据取决于程序。嗯,差不多了。在这张图中,我试图说明内存以及我们在哪里有一些数据。数据是以零结尾的字符串“Hello, World\n”,但前提是我们将其解释为 ASCII 编码的字符串。如果我们将其解释为整数数组,那就是这样。硬件并不关心你如何解释数据。

地址空间和 Hello World

计算机之所以成为 Neuman 模型,是因为数据和程序都在同一内存中表示。我们不仅可以通过其地址获取任何数据,还可以获取我们想要运行的代码。两者之间没有任何区别。程序、函数或方法只是一个地址,其中包含一系列数字,CPU 可以将这些数字解释为可执行代码。理论上,您可以指向“Hello, World\n”,然后告诉 CPU 将其作为程序运行。 (我不会推荐它)。

当涉及到可执行代码时,CPU 的解释略有不同。在您自己的程序中,您主要可以选择如何表示数据(尽管如果您想要与从原始硬件获得的表示不同的表示形式,可能会受到一些惩罚),但 CPU 会将不同的数字解释为特定指令并将其执行为这样的。至少如果你运行本机代码,它是这样工作的;如果你有一个虚拟机,那么虚拟机就是一个解释你的代码的程序,它对数据的解释可能与CPU的完全不同。不过,虚拟机通常会运行本机代码,因此您仍然依赖 CPU 的数字互操作,尽管是间接的。

我还应该提到,现代硬件和操作系统通常并不坚持简单的冯·诺依曼模型。如果您将程序和数据视为可互换的,则会遇到一些巨大的安全漏洞。实际上,您在不同的内存块上设置了某种形式的权限,并且您的代码必须位于允许您执行的块中,而您的数据(通常)则不允许。不过,如果您想自动生成本机可执行代码,您可以切换权限,虚拟机通常会这样做。

不管怎样,为了简单起见,我们假设我们有一个简单的冯·诺依曼模型。那么程序和数据都只是内存块,我们要么将其解释为程序(然后当我们告诉 CPU 在给定地址运行代码时,它将由 CPU 执行)或数据(然后我们的软件负责将内存中的数字解释为一些更高的数据结构)。

此级别上的对象、属性或其他更高级别的概念之间没有任何差异。这些完全在硬件之上的级别处理。它们只是对内存中原始数字的解释。

更新:更多细节...

存储对象

硬件对对象一无所知。它有地址,并且这些地址上有数字(或位模式,如果您愿意的话)。大多数数据类型跨越多个地址。例如,如果我们可以寻址字节,但整数占用四个字节(即它们是32位整数),那么自然我们需要在四个地址处的四个字节来表示一个整数。它们将表示为四个连续的字节,并且根据体系结构,您可能会将最重要的字节放在第一个或最后一个 (这称为字节顺序) 因此,数字 10(适合单个字节,但仍然是一个四字节整数)可能表示为 0x00 0x00 0x00 0x0a0x0a 0x00 0x00 0x000x0a 字节是 10,它可能是第一个或最后一个。

整数对象

那么结构呢?它最接近我们所认为的对象?它们是较大的属性/属性/条目/任何内容的块,并且它们以相同的方式表示。内存块是我们所拥有的一切。

如果您有一个包含两个整数的对象,例如一个矩形的表示,那么该对象位于内存中的某个位置,并将包含这两个整数的表示。

rect:
   h, w: int

我特意为此编写了语法,因为它不是特定于语言的,并且不同的语言和运行时系统在如何执行此操作方面有不同的变化,但它们都做了类似的事情。

这里,一个表示可以是一个 8 字节块,两个 4 字节整数,其中第一个是 h,第二个是 w。元素之间可能有填充,因此对象按照硬件喜欢的方式对齐,但我在这里将忽略这一点。

矩形对象

如果对象位于地址 0xafode4,则意味着 h 也位于该位置(假设对象中没有存储任何额外信息),并且这意味着如果整数占用四个字节的空间,则 w 位于四个字节之后。同样,细节会有所不同,但如果您在编译时知道对象的布局,通常就是这样做的。 (如果您直到运行时才知道它们,您将拥有一个属性表,并且对象包含该表)。

现在,如果一个对象包含其他对象会发生什么?比如说,如果矩形由两个点表示,并且这些点是对象,

point:
   x, y: int

rect:
   p1, p2: point

在最简单的版本中,没有任何变化。矩形对象包含两个点,因此这些点嵌入到表示矩形的内存中。

,这并不总是有效。如果您具有多态类型,您可能不知道所包含对象的具体类型,因此无法分配内存。在这种情况下,您将拥有对它的引用(一个指针),而不是包含另一个对象。 rect 对象将保存两个点的地址,并且这些点将位于内存中的其他位置。如果您想要构建重要的数据结构,这也是您必须做的,因此它不是特定于面向对象或对象的。

矩形对点的引用

在 OOP 环境中,可能需要做更多的工作,但我们会做到这一点。首先,让我们考虑函数(让我们回到只包含 hw 的矩形)。

函数的表示

代码也只是内存块,但其中的数字代表 CPU 的指令。假设我们想要将两个数字相乘,那么我们可能有一条看起来像

mul a, b, c

这样的指令,表示 CPU 应该获取寄存器 ab 中的数字,将它们相乘,并将结果放入寄存器c中。您通常有从内存中获取输入或作为常量等的指令,但我们只考虑一条简单的指令:将寄存器中的两个数字相乘并将结果放入第三个寄存器中。

mul 指令有一个数字。我们可以完全任意地说它是字节0xef。这三个参数指定寄存器,如果它们都是一个字节,则最多可以有 256 个寄存器。完整指令将包含四个字节、mul 指令0xef 和三个参数。如果我们要将寄存器r1与寄存器r2相乘并将结果放入寄存器r0,则指令将是

mul     r1,   r2,   r0
0xef  0x01  0x02  0x00

计算机看到的程序0xef 0x01 0x02 0x00

对于函数,我们还需要两件事:返回的方法,以及处理输入和输出的方法。

返回位很简单。会有一个 ret 指令返回到函数被调用的地方,在进程中处理堆栈寄存器等。我们可以假设 ret 的代码为 0xab

输入和输出由调用约定指定,并且与硬件本身无关。您需要一种商定的方法来将参数传递给函数,并且您需要知道函数返回时结果在哪里,但这就是全部。在我们想象的架构中,我们可以说输入一和输入二将位于寄存器 r1r2 中,并且输出应位于 r0 中:我们回来了。这样,我们就可以

fun mult(a, b): return a * b

用指令

mul r1, r2, r0  ; 0xef 0x01 0x02 0x00
ret             ; 0xab

创建一个简单的乘法函数,计算机会将其存储为数字0xef 0x01 0x02 0x00 0xab。如果您知道此代码/数据在内存中的位置,例如 0x00beef,您可以使用其他指令 call 调用函数 call 0x00beef (即还有一个数字,例如 0x10)和地址(这里的地址在桌面上通常为 8 个字节,或 64 位,因此 0x00beef 中的三个字节将具有它之前或之后的零,取决于字节顺序,我会假装我们有三个字节地址以使其更具可读性)。

要调用该函数,您首先需要将参数放入正确的寄存器中,因此如果您想获取 rect 对象的面积,您需要获取 hw 写入寄存器 r1r2

您想要做的是调用

area = mult(rect.h, rect.w)

,那么如何将 rect.h 和 rect.w 放入寄存器呢?您需要相关说明。假设我们有一个 mov 指令 (0x12),如下所示:

mov adr, reg

其中 adr 是一个地址(在该假想架构上为 3 个字节) reg 是一个寄存器(1 个字节)。完整指令为 5 个字节(0x12 指令、3 字节地址和 1 字节寄存器)。如果您的 rect 对象位于 0xaf0de4,那么我们在 0xaf0de4 处也有 rect.h,并且我们有rect.w 四个字节后,位于 0xaf0de8 处。调用 mult(rect.h, rect.w) 涉及这些指令

mov 0xaf0de4, r1    ; rect.h -> r1
mov 0xaf0de8, r2    ; rect.h -> r2
call 0x00beef       ; mult(rect.h, rect.w)
; now rect.h * rect.w is in r0

计算机上存储的实际数据就是这样的代码:

; mov 0xaf0de4,      r1
 0x12 0xaf 0x0d 0xe4 0x01
; mov 0xaf0de8,      r2
 0x12 0xaf 0x0d 0xe8 0x02
; call 0x00beef
 0x10 0x00 0xbe 0xef

一切仍然只是我们可以通过地址访问的数字。

中的,这在现实生活中是行不通的。当您编译程序时,您不知道所有对象将在哪里。一旦启动可执行文件,您就知道一些地址。例如,函数的位置是已知的,链接器可以在您需要的地方插入正确的地址。对象的位置,通常不是。但是会有像 mov 这样的指令从寄存器而不是程序中获取地址。例如,我们可以有一条指令

mov a[offset], b

将数据从存储在寄存器 a + offset 中的地址移动到寄存器 b 中。它可能有另一个数字,例如 0x13 而不是 0x12,但在汇编中通常具有相同的代码,因此您不会在那里看到它。

您还会有一条将常量放入寄存器的指令,如果它也称为 mov 并且具有

mov a, b

现在 a 的 形式,我不会感到惊讶一个常量,即某个数字,然后将该数字放入寄存器b中。程序集看起来相同,但指令可能具有编号 0x14

不管怎样,我们可以用它来调用 mult(rect.h, rect.w) 。那么代码将是

mov 0xaf0de4, r3  ; put the address of rect in r3
; 0x14 0xaf 0x0d 0xe4 0x03
mov r3[0], r1     ; put the value at r3+0 into r1
; 0x13 0x03 0x00 0x01
mov r3[4], r2     ; put the value at r3+4 into r2
; 0x13 0x03 0x04 0x02
call 0x00beef
; 0x10 0x00 0xbe 0xef

如果我们有这些指令,我们还可以将函数 mult(a,b) 修改为以矩形作为输入并返回面积

fun area(rect): rect.h * rect.w

的函数。函数可以获取对象作为其单个参数,它将进入寄存器r1,并从那里加载rect.hrect.w进行乘法他们。

; area(rect) -- address of rect in r1
mov r1[0], r2   ; rect.h -> r2
mov r1[4], r3   ; rect.w -> r3
mul r2, r3, r0  ; rect.h * rect.w -> r0
ret             ; return rect.h * rect.w

它变得比这更复杂,但你现在应该有想法了。我们的函数是此类指令的序列,它们的参数和结果值通常通过寄存器按照某种调用约定来回传递。如果要将值传递给函数,则需要将其放入正确的寄存器中(或堆栈中,具体取决于调用约定),然后函数将对其进行操作。它对对象所做的事情完全是软件;硬件并不关心那么多。

类和多态性

如果我们想要多态方法怎么办?如果我们有一个几何对象的类层次结构,并且 rect 只是其中之一,并且所有它们都应该有一个 area 方法,在调用时,根据对象的类?

当你拥有多态方法时,你真正拥有的是一堆不同的函数。如果您在恰好是圆形的对象 x 上调用 x.area(),那么您实际上是在调用 circle_area(x),而如果 xrect,则您正在调用 rect_area(x)。要完成这项工作,您唯一需要做的就是拥有一种分派到正确函数调用的机制。

这里,细节再次有所不同(很多),但一个简单的解决方案是将指针放入对象中的正确函数。如果您调用 x.area() ,您可能知道 x 内存中的第一个元素是指向其特定区域函数的指针。因此,您不是直接调用函数,而是从 x 获取函数的地址,然后调用它。

x.area() == (x.area_func)(x)

所有可以调用 area() 的对象都应该有这个函数,并且它们应该在距对象地址相同的偏移处有它,然后就可以这么简单了。

多态性函数指针

当然,如果您的类有很多方法,这可能会浪费内存。您在每个对象中存储指向每个方法的指针(并且您还必须花时间初始化它,因此也有额外的开销)。

那么另一个解决方案可以是添加一个间接级别。如果类的所有对象的方法都相同(通常是这样,但并非所有语言),那么您可以将方法表放在类对象中,并在每个对象中拥有指向该类的单个指针。当您需要获取正确的函数时,您首先获取类,然后从中获取函数。

x.area() == (x.class.area_func)(x)

多态性类

通过单一继承,不同类中的表可以具有不同的大小,并且不会因此而变得更加复杂。对于多重继承,它确实变得更加复杂,但是在不同的语言中处理方式非常不同,因此很难对此说任何一般性的话。

Computers do not really store abstract information of that sort at the basic level. There, you essentially have numbers--in binary, but that is not important--and it is generally up to software to interpret these numbers.

In the Von Neuman model, that close to every system is based on, you have one big address space. You can index into it, so your CPU can, for example, fetch the number that sits on a given address, or write a new number to an address, and that is mostly what there is to storing data. Usually, but not always, the addresses pick individual bytes of your memory, but your computer could address into larger or smaller word sizes, for example, you might have a computer that would address into 32 bit words instead of 8 bit words. It doesn't matter for the overall model, though. You just have a big block of memory and you can get the data at individual addresses.

How you interpret this data is up to the program. Well, almost. In this figure, I've tried to illustrate memory and where we have some data. The data is the zero-terminated string "Hello, World\n", but only if we interpret it as an ASCII-encoded string. If we interpreted it as an array of integers instead, then it would be that. The hardware doesn't care how you interpret the data.

Address space and Hello World

What makes a computer a Neuman model is that both data and program is represented in the same memory. Not only can we get to any data via its address, but we can get to the code we want to run as well. There isn't any difference between the two. A program, or a function, or a method, is just an address where you have a sequence of numbers, and the CPU can interpret these numbers as executable code. You can, in theory, point to "Hello, World\n" and then tell the CPU to run it as a program. (I won't recommend it).

When it comes to executable code, there is the slight difference that the CPU does the interpretation. In your own program, you can mostly choose how to represent data (although there might be some penalties if you want different representations than what you get from the raw hardware), but the CPU will interpret the different numbers as specific instructions and execute them as such. At least that is how it works if you run native code; if you have a virtual machine, then the virtual machine is a program that interprets your code, and its interpretation of the data can be quite different from the CPU's. The virtual machine, though, will typically run native code, so you are still relying on the CPU's interoperation of numbers, although indirectly.

I should also mention that modern hardware and operating systems do not usually stick with the simple Von Neuman model. If you treat program and data as interchangeable, you get some massive security holes. In practise, you have some form of permission set on different memory blocks, and your code has to sit in a block that you are allowed to execute, and your data (typically) is not. You can switch the permissions, though, if you want to autogenerate native executable code, and virtual machines often do this.

Anyway, for simplicity, let's just say that we have a simple Von Neuman model. Then both program and data are just chunks of memory that we either interpret as program (and it will then be executed by the CPU when we tell it to run the code at a given address) or as data (and then our software is responsible for interpreting the numbers in memory as some higher data structure).

There aren't any differences between object, properties, or other higher-level concepts at this level. Those are entirely dealt with at the level(s) above the hardware. They are simply interpretations of the raw numbers that sit in memory.

Update: a few more details...

Storing objects

The hardware doesn’t know anything about objects. It has addresses and there are numbers (or bit-patterns, if you prefer) at those addresses. Most data types span more than one address. If, for example, we can address bytes, but integers take up four bytes (i.e. they are 32-bit integers), then naturally we need four bytes, at four addresses, to represent an integer. They will be represented as four contiguous bytes, and depending on the architecture you might have the most-significant byte first or last (this is known as endianess) So, the number 10 (which fits in a single byte, but is still a four-byte integer) might be represented as 0x00 0x00 0x00 0x0a or 0x0a 0x00 0x00 0x00. The 0x0a byte is 10 and it might be first or last.

integer objects

What then about structures, which is what is closest to what we think of as objects? They are larger blocks of attributes/properties/entries/whatever, and they are represented the same way. Blocks of memory is all we have.

If you have an object that contains two integers, say a representation of a rectangle, then the object sits somewhere in memory and will contain the representation of those two integers.

rect:
   h, w: int

I’ve intentionally made up the syntax for this, since it isn’t language specific, and different languages and runtime systems have different variations on how they do this, but they all do something similar.

Here, one representation could be a block of 8 bytes, two 4-byte integers, where the first is h and the second is w. There might be padding between elements, so the objects are aligned the way the hardware prefers, but I will ignore that here.

rect object

If the object sits at address 0xafode4, that means that h also sits there (assuming that there is no extra information stored in the object), and that means that w sits four bytes later, if integers take up four bytes of space. Again, the details will differ, but this is generally how it is done if you know the layout of objects at compile time. (If you don’t know them until runtime, you will instead have a table of attributes, and the object contains the table instead).

Now, what happens if an object contains other objects? Say, what if the rectangle is represented by two points instead, and the points are objects

point:
   x, y: int

rect:
   p1, p2: point

In the simplest version, nothing changes. The rect object contains two points, so the points are embedded in the memory that represents the rect.

rect with points

This doesn’t always work, though. If you have polymorphic types, you might not know the concrete type of a contained object, so you cannot allocate memory. In that case, instead of containing the other object, you will have a reference to it, a pointer. The rect object would hold the addresses of the two points, and the points would sit elsewhere in memory. This is also what you have to do if you want to build non-trivial data structures, so it isn’t specific to object orientation or objects.

rect with references to points

In an OOP context, there might be a bit more work to it, but we will get to that. First, let’s consider functions (and let’s go back to a rectangle that just holds h and w).

Representation of functions

Code is just blocks of memory as well, but where the numbers represent instructions to the CPU. Let’s say we want to multiply two numbers, then we might have an instruction that looks like

mul a, b, c

that says that the CPU should take the numbers in registers a and b, multiply them, and put the result in register c. You usually have instructions that take the input from memory or as constants or such as well, but let’s just consider a single simple instruction: multiply two numbers you have in registers and put the result in a third register.

The mul instruction has a number. Completely arbitrarily we can say that it is the byte 0xef. The three arguments specify registers, and if they are a byte each we can have up to 256 registers. The full instruction would contain four bytes, the mul instruction 0xef and the three arguments. If we want to multiply register r1 with register r2 and put the result in register r0, the instruction would be

mul     r1,   r2,   r0
0xef  0x01  0x02  0x00

so what the computer sees is the program 0xef 0x01 0x02 0x00.

For functions, we need two things more: a way to return, and a way to handle input and output.

The return bit is easy. There will be a ret instruction that returns to where the function was called, handling stack registers and such in the process. We can pretend that ret has code 0xab.

Input and output is specified by a calling convention, and it isn’t tied to the hardware as such. You need an agreed upon way to pass arguments to functions and you need to know where the result is when the function returns, but that is all there is to it. On our imaginary architecture, we could say that input one and two will be in registers r1 and r2 and that the output should be in r0 when we return. That way, we can make a simple multiplication function

fun mult(a, b): return a * b

with the instructions

mul r1, r2, r0  ; 0xef 0x01 0x02 0x00
ret             ; 0xab

and the computer will store it as the numbers 0xef 0x01 0x02 0x00 0xab. If you know where this code/data sits in memory, e.g. 0x00beef, you can call the function call 0x00beef with some other instruction call (that also has a number, say 0x10) and the address (here an address is typically 8 bytes on a desktop, or 64 bits, so the three bytes in 0x00beef would have zeros before or after it, depending on endianes. I will pretend that we have three byte addresses to make it more readable).

To call the function, you first need to get the arguments into the correct registers, so if you want to get the area of our rect object, you want to get h and w into registers r1 and r2.

What you want to do is call

area = mult(rect.h, rect.w)

so how do you get rect.h and rect.w into registers? You need instructions for that. Let’s say that we have a mov instruction (0x12) that looks like this:

mov adr, reg

where adr is an address (3 bytes on this imaginary architecture) and reg is a register (1 byte). The full instruction is 5 bytes (the 0x12 instruction, the 3 byte address and the 1 byte register). If your rect object sits at 0xaf0de4, then we have rect.h at 0xaf0de4 as well, and we have rect.w four bytes later, at 0xaf0de8. Calling mult(rect.h, rect.w) involves these instructions

mov 0xaf0de4, r1    ; rect.h -> r1
mov 0xaf0de8, r2    ; rect.h -> r2
call 0x00beef       ; mult(rect.h, rect.w)
; now rect.h * rect.w is in r0

The actual data stored on the computer is the codes for this:

; mov 0xaf0de4,      r1
 0x12 0xaf 0x0d 0xe4 0x01
; mov 0xaf0de8,      r2
 0x12 0xaf 0x0d 0xe8 0x02
; call 0x00beef
 0x10 0x00 0xbe 0xef

Everything is still just numbers that we can access through addresses.

computing the area of a rect

Here, of course, the addresses we have used are hardwired into the program, and that doesn’t work in real life. You don’t know where all the objects will be when you compile your program. Some addresses you do know, once you fire up your executable. The location of functions, for example, will be known, and the linker can insert the correct addresses where you need them. Locations of objects, typically not. But there will be instructions like mov that takes the address from a register instead of from the program. We could, for example, have an instruction

mov a[offset], b

that moves data from the address stored in register a + offset into register b. It might have a another number, say 0x13 instead of 0x12, but in assembly you typically have the same code so you don’t see it there.

You would also have an instruction for putting a constant into a register, and I wouldn’t be surprised if that is also called mov and would have the form

mov a, b

where a is now a constant, i.e. some number, and you put that number in register b. The assembly looks the same, but the instruction might have number 0x14.

Anyway, we could use that to call mult(rect.h, rect.w) instead. Then the code would be

mov 0xaf0de4, r3  ; put the address of rect in r3
; 0x14 0xaf 0x0d 0xe4 0x03
mov r3[0], r1     ; put the value at r3+0 into r1
; 0x13 0x03 0x00 0x01
mov r3[4], r2     ; put the value at r3+4 into r2
; 0x13 0x03 0x04 0x02
call 0x00beef
; 0x10 0x00 0xbe 0xef

If we have these instructions, we could also modify our function mult(a,b) to one that takes a rectangle as input and returns the area

fun area(rect): rect.h * rect.w

The function can get the address of the object as its single argument, where it would go in register r1, and from there it could load rect.h and rect.w to multiply them.

; area(rect) -- address of rect in r1
mov r1[0], r2   ; rect.h -> r2
mov r1[4], r3   ; rect.w -> r3
mul r2, r3, r0  ; rect.h * rect.w -> r0
ret             ; return rect.h * rect.w

It gets more complicated than this, but you should have the idea now. Our functions are sequences of such instructions, and the arguments to them, and the result value, is passed back and forth, usually through registers, by some calling convention. If you want to pass a value to a function, you need to put it in the right register (or on the stack, depending on the calling convention), and then the function will operate on it. What it does with the object is entirely software; the hardware doesn’t care that much.

Classes and polymorphism

What then if we want polymorphic methods? If we have a class hierarchy of geometric objects and rect is just one of them, and all of them should have an area method that, when called, is dispatched based on the objects’ class?

When you have polymorphic methods, what you really have is a bunch of different functions. If you call x.area() on an object x that happens to be a circle, then you are really calling circle_area(x), while if x is a rect you are calling rect_area(x). The only thing you need to make this work is having a mechanism for dispatching to the right function call.

Here, again, the details differ (a lot), but a simple solution is to put pointers to the correct function in the objects. If you call x.area() maybe you know that the first element in the memory of x is a pointer to its specific area function. So, instead of calling a function directly, you fetch the address of the function from x and then you call it.

x.area() == (x.area_func)(x)

All objects you can call area() on should have this function, and they should have it at the same offset from the address of the object, and then it can be as simple as that.

polymorphism through function pointers

This can, of course, be wasteful in memory if your classes have lots of methods. You are storing a pointer to each method in each object (and you also have to spend time on initialising this, so there is additional overhead there as well).

Then another solution can be to add a level of indirection. If the methods are the same for all objects of a class (which they often are, but not for all languages) then you can put the table of methods in a class object and have a single pointer to the class in each object. When you need to get the right function you first get the class and then you get the function from it.

x.area() == (x.class.area_func)(x)

polymorphism with classes

With single inheritance, the tables in the different classes can have different sizes, and it doesn’t get more complicated because of that. With multiple inheritance, it does get more complicated, but that is handled very differently in different languages so it is hard to say anything general about that.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文