类的定义和结构体类似,但编译器井没有为类自动生成可以传入成员值的初始化器
编译器只为类自动生成了一个无参的初始化器。
编译器为结构体自动生成了无参和有参的初始化器。
如果没有初始值,类直接报错,编译器连无参的初始化器都不会为它自动生成。
初始化器
如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器
成员的初始化是在这个初始化器中完成的
上面2段代码是相等的。
结构体与类的本质区别
结论:
结构体是值类型(枚举也是值类型) , 类是引用类型(指针类型)
上图所示是一个结构体和类的对象内存分布。
变量point存储在栈空间,因为它是值类型,所以它的成员x,y都在栈空间中。
变量size也存储在栈空间,因为类是引用类型,所以它的内存中存储的是size对象的内存地址。而对象存储在堆空间,该对象占用32个字节。前8个字节存储的是指向类型信息的内存地址。第二个8字节存储的是指向引用计数的内存地址。最后16个字节存储的是成员width和height的值。
证明
使用工具Mems.swift来打印变量size和point的地址,以及它们的内容。 Mems.swift下载地址
代码如下:
print("size变量的地址===", Mems.ptr(ofVal: &size))
print("size变量的内容===", Mems.memStr(ofVal: &size))
print("size所指向内存的地址===", Mems.ptr(ofRef: size))
print("size所指向内存的内容===", Mems.memStr(ofRef: size))
print("point变量的地址===", Mems.ptr(ofVal: &point))
print("point变量的内容===", Mems.memStr(ofVal: &point))
print("size变量占用空间===", MemoryLayout.stride)
print("point变量占用空间===", MemoryLayout.stride)
打印结果:
size变量的地址=== 0x00007ffeefbff460
size变量的内容=== 0x00000001006667a0
size所指向内存的地址=== 0x00000001006667a0
size所指向内存的内容=== 0x000000010000c298 0x0000000200000003 0x0000000000000001 0x0000000000000002
point变量的地址=== 0x00007ffeefbff450
point变量的内容=== 0x0000000000000003 0x0000000000000004
size变量占用空间=== 8
point变量占用空间=== 16
通过打印结果可以发现,变量size和point的地址是紧挨在一起的,只相差了16个字节,这16个字节其实就是point所占用的空间。
point的内容分别是3和4,这也和我们的初始化的值吻合。
size的内容是一个地址,这个地址其实就是对象所在的堆区地址空间。
size指向的对象内容前2个8字节是两个内存地址,后第2个8字节分别存的是值3和4,和我们的初始化值吻合。
size变量占用空间是8个字节,因为它是一个指针类型,只存一个地址。
point变量占用空间是16个字节,因为它是一个值类型,会根据成员来开辟不同的空间。
值类型
- 值类型赋值给var、let或者给函数传参,是直接将所有内容拷贝一份,类似于对文件进行copy、paste操作 ,产生了全新的文件副本。属于深拷贝( deep copy )
既然结构体是值类型,那么下面的代码p1和p2应该是两个完全不同的空间,并且更改p2的值,p1并不会被修改。
struct Point {
var x: Int
var y: Int
}
var p1 = Point(x: 10, y: 20)
var p2 = p1
p2.x = 11
p2.y = 22
查看汇编代码证明:
下面的汇编代码大致流程是调用Point.init方法,将10赋值给p1.x,20赋值给p1.y。然后将10赋值给p2.x,20赋值给p2.y。最后将12赋值给p2.x,22赋值给p2.y。
0x1000035a1 <+1>: movq %rsp, %rbp
0x1000035a4 <+4>: subq $0x20, %rsp
0x1000035a8 <+8>: xorps %xmm0, %xmm0
0x1000035ab <+11>: movaps %xmm0, -0x10(%rbp)
0x1000035af <+15>: movaps %xmm0, -0x20(%rbp)
0x1000035b3 <+19>: movl $0xa, %edi // 把10给edi
0x1000035b8 <+24>: movl $0x14, %esi // 把20给esi
0x1000035bd <+29>: callq 0x1000035f0 ; Point.init(x: Swift.Int, y: Swift.Int) -> Point in swiftTest.test() -> () at main.swift:34
// 跳到下面的init方法里去
-> 0x1000035c2 <+34>: movq %rax, -0x10(%rbp) // 把rax里的内容(10)给-0x10(%rbp) 也就是p1.x
0x1000035c6 <+38>: movq %rdx, -0x8(%rbp) // 把rdx里的内容(20)给-0x8(%rbp) 也就是p1.y
0x1000035ca <+42>: movq %rax, -0x20(%rbp) // 把rax里的内容(10)给-0x20(%rbp) 也就是p2.x
0x1000035ce <+46>: movq %rdx, -0x18(%rbp) // 把rdx里的内容(20)给-0x18(%rbp)也就是p2.y
0x1000035d2 <+50>: movq $0xb, -0x20(%rbp) // 把12给-0x20(%rbp) 也就是p2.x
0x1000035da <+58>: movq $0x16, -0x18(%rbp) // 把22给-0x18(%rbp) 也就是p2.y
0x1000035e2 <+66>: addq $0x20, %rsp
0x1000035e6 <+70>: popq %rbp
// ------ init方法汇编代码-------
swiftTest`init(x:y:) in Point #1 in test():
0x1000035f0 <+0>: pushq %rbp
-> 0x1000035f1 <+1>: movq %rsp, %rbp
0x1000035f4 <+4>: movq %rdi, %rax // 把rdi(上面的edi)里的内容(10)给rax
0x1000035f7 <+7>: movq %rsi, %rdx // 把rsi(上面的esi)里的内容(20)给rdx
0x1000035fa <+10>: popq %rbp
0x1000035fb <+11>: retq
引用类型
引用赋值给var、let或者给函数传参,是将内存地址拷贝一份。类似于制作一个文件的替身(快捷方式、链接),指向的是同一个文件。属于浅拷贝( shallow copy ),修改替身的值也就相当于在修改原型的值。
如上图所示,类是引用类型,所以s2=s1时就是浅拷贝,s1和s2内存空间里存储的都是Size(width:10, height:20)
这个实例的地址。
汇编证明类是引用类型
运行如下代码:
class Size {
var width: Int
var height: Int
init(width: Int, height: Int) {
self.width = width
self.height = height
}
}
func test() {
var s1 = Size(width: 10, height: 20)
var s2 = s1
s2.width = 11
s2.height = 22
}
test()
断点后查看汇编代码
调用allocating_init方法后在堆空间开辟地址存储实例,将返回的堆空间地址复制给了s1和s2两个变量的空间,所以s1和s2存储的都是同一块地址。
查看allocating_init后rax寄存器存储地址对应的内存内容,可以看到就是实例的内存分布,有10和20两个数。
最后看汇编中修改s2.width和s2.height,其实修改的就是实例在堆空间中那个地址第16个字节和第32个字节。