# Swift 结构体和类的内存布局
## 结构体内存布局
实验1:在 struct 内部自己实现 init
```swift
struct Point {
var x: Int
var y: Int
init () {
x = 0
y = 0
}
}
var point = Point()
```
在`init` 方法内第一行处加 断点,如下所示
实验2:struct 内不自己加 init
```swift
struct Point {
var x: Int = 0
var y: Int = 0
}
var point = Point()
```
在`var point = Point()`处加 断点,如下所示
结论:结构体会有一个编译器自动生成的初始化编译器。目的是保证所有成员都有初始值。
实验3:
```swift
struct CustomDate {
var year: Int
var month: Int
var isLeapYear: Bool
}
var date = CustomDate(year: 2024, month: 3, isLeapYear: false)
print(MemoryLayout.size) // 17
print(MemoryLayout.stride) // 24
print(MemoryLayout.alignment) // 8
print(Mems.memStr(ofVal: &date)) // 0x00000000000007e8 0x0000000000000003 0x0000000000000000
```
Int 占8 Byte,Bool 占1 Byte,共 2*8 + 1 = 17 Byte,由于存在内存对齐,所以17向上到24 Byte。
- 值语义:`struct` 是值类型,这意味着当你将一个 `struct` 赋值给另一个变量或传递给函数时,会创建一个新的副本。每个副本都有其自己的内存空间,对其中一个副本的修改不会影响其他副本。
- 内存连续性:`struct` 的成员变量在内存中是连续存储的,没有额外的内存开销(如对象指针或元数据)。这使得访问 `struct` 的成员变量非常高效。
- 内存对齐:为了确保访问效率,编译器可能会对 `struct` 的成员变量进行内存对齐。这意味着某些成员变量之间可能会有未使用的内存空间(填充字节)。这种对齐通常是基于目标平台的硬件架构和访问性能考虑。
- 嵌套结构体:如果 `struct` 包含其他 `struct` 或枚举作为成员,那么这些嵌套的类型也会按照它们自己的内存布局规则进行排列。
- 可变大小结构体:在某些情况下,`struct` 的大小可能不是固定的。例如,如果 `struct` 包含可变长度的数组或字符串,那么它的实际大小将取决于这些成员的大小。然而,即使在这种情况下,`struct` 的内存布局仍然是紧凑的,并且遵循相同的访问规则。
- 与类的比较:与 `class`(类)不同,`struct` 不需要额外的内存来存储类型信息、引用计数或其他元数据。这使得 `struct`通常比 `class` 更轻量级,并且在某些情况下具有更好的性能。
## 类的内存布局
类和结构体类似,但是编译器不会为类自动生成可以传入成员值的初始化器。
```swift
// 写法1
class CustomDate {
var year: Int = 2024
var month: Int = 3
}
// 写法2
class CustomDate {
var year: Int
var month: Int
init () {
year = 2024
month = 3
}
}
```
上面2个写法是等价的。
结构体和类的区别:
- 结构体是值类型(枚举也是值类型),类是引用类型(指针类型)
值类型赋值给 var、let 或者给函数传参,是直接将所有内容拷贝一份。产生了全新副本,属于深拷贝。
### 值类型
```swift
func test() {
struct Point {
var x: Int
var y: Int
}
var point1 = Point(x: 10, y: 20)
var point2 = point1
point2.x = 11
point2.y = 22
print(point1.x) // 10
print(point1.x) // 20
print("over")
}
test()
```
因为是在函数内部变量,所以是在栈上分布
| 内存地址 | 内存数据 | 说明 |
| -------- | ---------------------------------- | -------- |
| 0x10000 | 10 ----赋值改变------> 11 | point2.x |
| 0x10008 | 20 ----赋值改变------> 22 | point2.y |
| 0x10010 | 10 | point1.x |
| 0x10018 | 20 | point1.y |
断点打在 `var point1 = Point(x: 10, y: 20)` 处,查看汇编
乍一看如果不认识的话,先找字面量(立即数),比如红色框内的 `0xa`,就是 10,`0x14` 就是20。[之前](./109.md)学过寄存器的设计,64位寄存器是兼容32位寄存器的。红色框内将 `0xa`,也就是 10 保存到 `%edi ` 寄存器内部,也就是保存到 `%rdi` 中,将 `0x14` 也就是20,保存到 `%esi` 也就是保存到 `%rsi` 寄存器中。
LLDB 模式下输入 `si` 进入 init 方法内部。
可以查看到将 `%rsi` 里的10 保存到 `%rdx` 中了;将 `%rdi` 里的20 保存到 `%rax` 中了。这也就是 struct `init` 方法做的事情。
LLDB 模式下输入 `finish` 结束 init 方法。
可以看到下面几句汇编
```assembly
0x1000035ac <+44>: movq %rax, -0x10(%rbp) // rbp - 0x10
0x1000035b0 <+48>: movq %rdx, -0x8(%rbp) // rbp - 0x8
0x1000035b4 <+52>: movq %rax, -0x20(%rbp) // rbp - 0x20
0x1000035b8 <+56>: movq %rdx, -0x18(%rbp) // rbp - 0x18
0x1000035bc <+60>: movq $0xb, -0x20(%rbp)
0x1000035c4 <+68>: movq $0x16, -0x18(%rbp)
```
可以看到分别将 `%rax` 里的10赋值给内存地址为 `%rbp - 0x10` ,`%rdx` 里的20赋值给内存地址为 `%rbp - 0x8` 了。
可与看到 `0x10` 和 `0x8` 地址相差8,且地址连续,也就是 point1 的内存地址。同样下面的 `0x20` 和 `0x18` 地址相差8,且地址连续,也就是 point2 的内存地址。
第五行将 `0xb` 也就是11 赋值给 `%rbp - 0x20`的地址,`0x16` 也就是22赋值给 `%rbp-0x18`的地址,也就是 point2 的 x、y
**Swift 标准库中,为了提升性能,String、Array、Dictionary、Set 采取了 Copy On Write 技术**
比如仅当有“写”操作时,才会真正执行拷贝操作
对于标准库值类型的赋值操作,Swift 能确保最佳性能,所以没必要为了保证最佳性能来避免赋值。
### 引用类型
引用赋值给 var、let 或者给函数传参,是将内存地址拷贝一份。属于浅拷贝
```swift
func testReferenceType() {
class Size {
var width: Int
var height: Int
init(width: Int, height: Int) {
self.width = width
self.height = height
}
}
var size1 = Size(width: 10, height: 20)
var size2 = size1
size2.width = 11
size2.height = 22
}
testReferenceType()
```
下断点,可以看到下面的汇编:
在调用(汇编的 call)完 `allocating_init` 方法后,方法返回值用 `%rax` 保存的。然后打印出 `%rax` 寄存器的值,查看内存信息如下
红色框代表类信息的地址,蓝色框代表引用计数,绿色框代表10,黄色框代表20.
汇编的第17行,`movq %rdi, -0x50(%rbp)` 应该就是将 `%rdi` 里面的 对象内存地址赋值到 `%rbp - 0x50` 中去,也就是 `size1` 指针地址。
汇编的第20行,`movq %rdi, -0x10(%rbp)` 应该就是将 `%rdi` 里面的 对象内存地址赋值到 `%rbp - 0x10` 中去,也就是 `size12 指针地址。
再接下去的汇编
```swift
0x100003525 <+133>: movq -0x50(%rbp), %rax
0x100003529 <+137>: movq $0xb, 0x10(%rax)
0x100003531 <+145>: callq 0x100007434 ; symbol stub for: swift_endAccess
0x100003536 <+150>: movq -0x50(%rbp), %rdi
0x10000353a <+154>: callq 0x100007476 ; symbol stub for: swift_release
0x10000353f <+159>: movq -0x68(%rbp), %rdx
0x100003543 <+163>: movq -0x60(%rbp), %rcx
0x100003547 <+167>: movq -0x50(%rbp), %rdi
0x10000354b <+171>: addq $0x18, %rdi
0x10000354f <+175>: leaq -0x48(%rbp), %rsi
0x100003553 <+179>: movq %rsi, -0x58(%rbp)
0x100003557 <+183>: callq 0x100007410 ; symbol stub for: swift_beginAccess
0x10000355c <+188>: movq -0x58(%rbp), %rdi
0x100003560 <+192>: movq -0x50(%rbp), %rax
0x100003564 <+196>: movq $0x16, 0x18(%rax)
```
可以看到将 `%rbp - 0x50 ` 的值赋值给 `%rax` ,然后将 `oxb` 也就是 11 保存到 `%rax` 也就是 size1 指针的所指向的内存的 `0x10`处,为什么是前面空了16位?因为前8位保存类信息、后8位保存引用计数信息,所以从16位开始。
`movq $0x16, 0x18(%rax)` 将 `0x16` 也就是 22 保存到 `%rax` 也就是 size1 指针的所指向的内存的 `0x18`处,为什么是前面空了24位?因为前8位保存类信息、中间8位保存引用计数信息,后8位保存 Int 的 width,所以从24位开始。