| |
这段代码涉及了结构体的相互引用,为什么能通过编译并工作呢?
编译器是从上到下逐行扫描代码的,我们来看看当它遇到这两个结构体的时候发生了什么。
| |
当编译器读到这里,它需要知道 UIBase 的大小,第一个字段是个指针。
在 32 位系统上,指针永远是 4 字节;在 64 位系统上,指针永远是 8 字节。虽然编译器完全不知道 Page 结构体是什么样的,也不影响它计算出 UIBase 的大小,所以这一段是编译通过了。
| |
当编译器读到这里,它同样需要知道 Page 的大小。因为 Page 包含了 UIBase(而不是 UIBase*),所以 UIBase 的大小也是需要确切知道的。
好在,UIBase 的定义此时已经被编译器阅读完毕了,所以 Page 的大小也是能被确切计算得到的。
不能工作的情况
| |
要计算 A 的大小,就要先计算 B 的大小;
要计算 B 的大小,同样需要先计算 A 的大小。
这就变成了套娃。
再看一开始能工作的情况:一个是实体,另一个只是个指针。关键在于指针的大小是固定的,只要前面的声明使用的是后面声明类型的指针,编译就能通过。
为什么只需要知道类型大小,就能通过编译并运行?
这里涉及一个计算机科学的原理: 类型 (Type) 只是给编译器看的,对于 CPU 而言,一切只是内存块 (Memory Block) 和地址偏移 (Offset)。
当我们通过代码创建一个类型的实例,会通过编译器告诉 CPU 预留足够的空间:
假设 UIBase 占 60 Byte,编译器会生成指令 SP = SP - 60,这会在栈上预留 60 Byte 的空间。
这就结束了,CPU 并不关心这里面存储的到底是 Page* 还是别的什么类型的数据,CPU 只需要知道分配多少空间。
当我们使用这个变量,如 ui.isPage = true; ,CPU 也不需要知道这个字段代表什么,因为编译器告诉它的是:
请把这块内存区域(也就是前面分配的 60 Byte 内存块)中偏移量 x 的 n 字节数据的内存改为 1 (true)。
也就是说,CPU 读写是通过基地址 + 偏移量完成的,是编译器让变量和成员有了程序员所理解的“意义”。CPU 并不关心这些意义,它只是按照设定的方式执行机器码。
假设我们执行了代码:
| |
这行代码通过一个 UIBase 变量访问了其中 Page* 指针所指向对象的一个 Page 的成员,但 CPU 其实并不关心什么是 UIBase 或者 Page,它只关心:地址和固定的数字。
CPU 先到 uibase 对象所在的内存地址,按照 parent 的偏移读取到了 parent 指向的地址,然后使用这个地址按照 x 的偏移获取到了最终需要操作的地址,最后把这个地址对应的内存块设置为代表了数值 100 的二进制数值。