1、结构体
结构体都有一个编译器自动生成的初始化器。
根据情况可能会生成多个初始化器,保证所有的成员(存储属性、Stored Property)都有初始值。
struct Person {
var name: String
var age: Int
}
//正确
var person = Person(name: "wang", age: 18)
//错误
var person1 = Person(name: "li")
var person2 = Person(age: 20)
var person3 = Person()
可以看到我们在确保所有的储存属性有值的情况下去初始化才是正确的。
那我们也可以这样去定义属性
struct Person {
var name: String = ""
var age: Int = 0
}
var person = Person(name: "wang", age: 18)
var person1 = Person(name: "li")
var person2 = Person(age: 20)
var person3 = Person()
这种情况下下面的即中初始化方法均可,因为我们已经给我们的存储属性赋了初值了。
那同样,我们也可以给定其初始值为nil
,这样也可以使用下面的初始化方法。
当然,我们也可以自定义初始化器。
struct Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
同样我们也可以在自定义的初始化器中给定其默认值。
struct Person {
var age: Int?
init() {
self.age = 10
}
}
var person = Person()
此时我们通过汇编进入看一下,可以看到其调用了init
函数
通过命令
si
进入该函数
可以看到在第8行处将10赋值到一处内存中。
但如果我们没有自定义初始化器呢?也就是直接给定其一个默认值的情况下,其内部是怎么调用的呢?
struct Person {
var age: Int = 10
}
var person = Person()
通过断点走入汇编代码。可以看到与上面相比基本是一模一样的,甚至连调用方法的地址都是一致的。
可以看到仍然是调用了
init
方法,同样进入该方法中,可以看到也是有赋值操作的。
也就是说,不管我们是通过自定义初始化器给定属性默认值还是直接定义属性时给定其默认值都是在
init
方法中将数据写入属性中去的。
至于结构体在内存中的布局其实与上一期我们讲到的枚举是一致的。
比如上面我们实例化的person
对象,其有一个属性为age
,该属性在内存中占8
个字节,又因为其内存对齐字节为8
,因此其内存中的布局为0x000000000000000a
。
2、类
相比结构体而言类是没有可以传入成员值的初始化器的。
同样,当我们定义了类以后没有给其成员默认值的情况下也是会有错误提示的
但是类因为没有默认的成员初始化器,因此我们直接在初始化时传入成员属性时不可行的:
此时需要我们手动去给其实现初始化器。
3、区别
其实结构体和类的本质区别在于结构体是值类型而类是引用类型。
也就是说结构体对象其位于内存中即栈空间中,而引用类型其真正的内存空间在堆空间中。
但严格来说我们并不能说结构体内存一定是在栈空间,其内存分配在哪儿取决于它定义的位置,如果其在函数内定义的那么其内存在栈空间内。如果其在外部定义,那么其内存在数据段内(全局区)。如果其作为一个类的属性,那么其内存自然是会分配在堆空间。
至于类的话不管在哪儿定义其内存一定是在堆空间,在不同的地方定义改变的只是其指针的内存位置。
问题:那如果结构体定义在一个类的方法里边其内存会在哪儿?
3.1 内存分配
那么我们怎么去区分一个对象是在栈空间还是在堆空间?
其实如果对象是在堆空间时其初始化时会牵涉到alloc
或者malloc
。
此时我们定义一个类对象并实例化,此时加上断点
汇编开启后会看到
此时可以看到在第14行处明显的有调用
allocating_init
,即向堆空间申请内存,此时通过命令si
进入
可以看到在第16行处有
allocObject
,此时进入,然后一直通过si
命令进入。
最终到这个地方:
可以看到在这里调用了一个
slowAlloc
方法。
进入后可以看到在这里会调用系统级别的
malloc
方法分配内存
而同样的我们可以看到结构体会调用init
,也就是直接分配在栈空间的。
//类对象堆空间申请内存过程
接下来我们看看struct
和class
在内存中的地址。定义一个结构体以及类,并实例化
struct StructObject {
var age: Int = 10
var count: Int = 11
}
class ClassObject {
var age: Int = 10
var count: Int = 20
}
var strutObject = StructObject()
var classObject = ClassObject()
print("结构体变量的地址:",Mems.ptr(ofVal: &strutObject))
print("结构体变量的内容:",Mems.memStr(ofVal: &strutObject))
print("----------------")
print("类变量指针的地址:",Mems.ptr(ofVal: &classObject))
print("类变量指针的内容:",Mems.memStr(ofVal: &classObject))
此时我们查看打印结果:
结构体变量的地址: 0x00000001000083c8
结构体变量的内容: 0x000000000000000a 0x000000000000000b
----------------
类变量指针的地址: 0x00000001000083d8
类变量指针的内容: 0x000000010079c950
其中0x00000001000083c8
和0x00000001000083d8
分别是两个对象的地址。我们查看内存中的值,可以看到前8个字节是Person.age
的值,后面8个字节是Person.count
的值
而类对应的内存中只是存放了另一个地址,而这个地址实际上指向的就是堆空间。
可是我们根据该地址去查看内存中的数据时发现前8个字节以及第2个8个字节中放的数据不知道是啥,而从第3个把个字节才看到是对应的属性的值。
其实类对应的存储空间中,前8个字节指向的是内存信息,第2个8字节是引用计数信息,后面就是成员信息了。
我们通过计算类变量指针地址与结构体变量的指针地址可以获知其差了
0x10
,也就是16个字节,那么0x00000001000083c8
这个地址存放0x000000000000000a
,接下来8个字节存放0x000000000000000b
,接下来的地址0x00000001000083d8
存放的就是0x000000010079c950
内容,也就是这三个挨着的地址前两个放着结构体的内容,后一个存放着类对象的地址。
我们也可以打印出类对象对应的信息:
print("类变量指针所指向的内容的地址:",Mems.ptr(ofRef: classObject))
print("类变量指针所指向的内容的内容:",Mems.memStr(ofRef: classObject))
类变量指针所指向的内容的地址: 0x00000001006086f0
类变量指针所指向的内容的内容: 0x00000001000082c8 0x0000000200000002 0x000000000000000a 0x0000000000000014
可以看到指针中存放的就是类对象的地址,类对象的4个8字节即是前面所说的一些信息。
我们也可以分析结构体对象和类对象在内存中的大小。
上述我们定义的结构体对象有2个属性,那么其在内存中的大小为16字节
。
类对象同样也有两个属性,但是指针仅占8字节
,而这8个字节中存的是类对象的地址,而真正的类对象是占用32个字节的。
上述结论可通过MemoryLayout
打印结果可知。
print("结构体内存大小",MemoryLayout.stride)
print("类对象指针内存大小",MemoryLayout.stride)
结构体内存大小 16
类对象指针内存大小 8
3.2 值类型汇编分析
值类型为深拷贝(deep copy) ,引用类型为浅拷贝(shallow copy)
深拷贝也就是赋值一份副本,即重新分配一块内存而浅拷贝仅仅是定义一个指针指向相同的内存。
func test() {
struct Auction {
var id: Int
var type: Int
}
let auction = Auction(id: 10, type: 20)
var p = auction
p.id = 11
p.type = 22
print("qeqwwq")
}
test()
同样断点分析汇编
直接找立即数,可以看到在第8行和第9行处将10和20分别放入
edi
和esi
中,而实际上edi
和esi
内容在rdi
和rsi
中可取得,地市我们进入init
方法中:
可以看到分别从
rdi
和rsi
中取值放入rax
和rdx
中,这个时候rax
和rdx
分别存放的就是10
和20
,此时再看第一张图片中call
指令下面可以看到分别将rax
和rdx
中的值放入了-0x10(%rbp)
和-0x8(%rbp)
中,同时也放入了-0x20(%rbp)
和-0x18(%rbp)
中,其实这也就对应了代码的实例化auction
以及p = auction
,第15行和16行又将11
和22
放入到p
对应的内存中,也就是重新给p
赋值操作。
此时我们是将结构体定义在函数内,实际上我们将结构体定义在外部也是一样,只是稍显复杂而已,但是其逻辑完全相同。
在swift标准库中字符串、数组、字典其底层都是结构体,因此其也都是值类型。
而swift中为了提升性能它们采用 Copy On Write机制。也就是修改的时候才去copy,如果不进行修改的时候仍然是同一份内存。
当对同一个结构体对象进行第二次实例化的时候其只是对原地址中的数据进行重新赋值,其地址不会改变。
var auction = Auction(id: 10, type: 20)
auction = Auction(id: 11, type: 22)
查看汇编,直接找到立即数
10
和20
可以看到分别赋值给edi
和esi
,也就是rdi
和rsi
。call
调用可函数,进入可以看到,从rdi
和rsi
中取出内容又给了rax
和rdx
。
SwiftStudy`init(id:type:) in Auction #1 in test():
-> 0x1000010e0 <+0>: pushq %rbp
0x1000010e1 <+1>: movq %rsp, %rbp
0x1000010e4 <+4>: movq %rdi, %rax
0x1000010e7 <+7>: movq %rsi, %rdx
0x1000010ea <+10>: popq %rbp
0x1000010eb <+11>: retq
此时我们看汇编断点的截图,函数调用完成后又把rax
和rdx
中的值给了-0x10(%rbp)
和-0x8(%rbp)
,此时我们查看对应的内存空间:
可以看到对应的正是
10
和20
,那么对应的地址0x7FFEEFBFF530
则是通过拿到rbp
的地址后计算得到对应的地址。
可以看到后面又重新走了赋值,调用
call
方法然后再赋值的操作。可以看到后面的赋值和前面的赋值的地址是一致的。
3.3 引用类型汇编分析
定义一个类
func testClass() {
class Person {
var age: Int
var height: Int
init(age: Int, height: Int) {
self.age = age
self.height = height
}
}
var person = Person(age: 10, height: 20)
var person1 = person
person1.age = 11
person1.height = 22
}
同样,进入汇编断点后直接找对应的立即数,可以看待第13、14行对应的分别是10
和20
,分别放入寄存器edi
和esi
,然后执行call
指令。
接下来调用了allocating_init
,进入该函数可以看到从对应的寄存器中取值后给到了相邻的两个地址中。
结束函数调用,回到执行call
处
可以知道,函数调用结束后将rax
中的值给了-0x10(%rbp)
和-0x60(%rbp)
,
而根据我们的代码可知,在实例化person
后会将person
赋值给person1
。那么和这里的汇编代码吻合了,也就是rax
存放的就是实例化Person
的地址,该地址分别给放在了对应的两个地址中。
-
rax
一般存放函数调用的返回值 -
-0x60(%rbp)
类似于这种rbp-地址值
的类型一般都是局部变量
获取rax
存放的地址值,直接去查看改地址对应的内存
(lldb) register read rax
rax = 0x00000001005388f0
(lldb) memory read 0x00000001005388f0
0x1005388f0: c0 72 00 00 01 00 00 00 02 00 00 00 00 00 00 00 .r..............
0x100538900: 0a 00 00 00 00 00 00 00 14 00 00 00 00 00 00 00 ................
(lldb)
可以看到,存放数据的共有32
个字节,第一个8字节
存放的是类信息,第2个8字节
存放的是引用计数信息,之后便是其两个属性数据了。
接下来我们去看修改person1
的属性值。同样我们去找对应的立即数
可知,将两个立即数放入到了对应的两个对应的空间。此时断点在37行,获取
rax
的地址,此时的地址既是对应的person
和person1
内存放的地址。
(lldb) register read rax
rax = 0x00000001005388f0
而又分别在0x00000001005388f0
的基础上移动了16
个字节(0x100538900
)和24
个字节(0x100538908
)对应的不就是两个属性数据的地址,也就是这里直接是修改了属性的值了,从而person
和person1
指向的堆空间的数据也改变了。
- 类似
rax+地址值
一般都是堆空间的地址