值类型
前提:需要了解内存五大区,内存五大区可以参考这篇文章iOS-底层原理 24:内存五大区,如下所示
值类型-1
-
栈区的地址 比 堆区的地址 大
-
栈是从
高地址->低地址
,向下延伸,由系统
自动管理,是一片连续的内存空间 -
堆是从
低地址->高地址
,向上延伸,由程序员
管理,堆空间结构类似于链表
,是不连续的 -
日常开发中的溢出是指
堆栈溢出
,可以理解为栈区与堆区边界碰撞的情况 -
全局区、常量区
都存储在Mach-O
中的__TEXT cString
段
我们通过一个例子来引入什么是值类型
func test(){
//栈区声明一个地址,用来存储age变量
var age = 18
//传递的值
var age2 = age
//age、age2是修改独立内存中的值
age = 30
age2 = 45
print("age=\(age),age2=\(age2)")
}
test()
从例子中可以得出,age存储在栈区
-
查看
age
的内存情况,从图中可以看出,栈区直接存储
的是值
-
获取age的栈区地址:
po withUnsafePointer(to: &age){print($0)}
-
查看age内存情况:
x/8g 0x00007ffeefbff3e0
-
值类型-2
-
查看
age2
的情况,从下图中可以看出,age2
的赋值相当于将age
中的值拿出来,赋值给了age2
。其中age
与age2
的地址 相差了8
字节,从这里可以说明栈空间是连续
的、且是从高到低
的
值类型-3
所以,从上面可以说明,age就是值类型
值类型 特点
-
1、地址中存储的是
值
-
2、值类型的传递过程中,相当于
传递
了一个副本
,也就是所谓的深拷贝
-
3、值传递过程中,并不共享状态
结构体
结构体的常用写法
//***** 写法一 *****
struct CJLTeacher {
var age: Int = 18
func teach(){
print("teach")
}
}
var t = CJLTeacher()
//***** 写法二 *****
struct CJLTeacher {
var age: Int
func teach(){
print("teach")
}
}
var t = CJLTeacher(age: 18)
-
在结构体中,如果不给属性默认值,编译是不会报错的。即在结构体中属性可以赋值,也可以不赋值
值类型-4
-
init
方法可以重写,也可以使用系统默认的
结构体的SIL分析
-
如果
没有init
,系统会提供不同的默认初始化方法值类型-5
-
如果
提供了自定义的init
,就只有自定义的值类型-6
为什么结构体是值类型?
定义一个结构体,并进行分析
struct CJLTeacher {
var age: Int = 18
var age2: Int = 20
}
var t = CJLTeacher()
print("end")
-
打印t:
po t
,从下图中可以发现,t的打印直接就是值,没有任何与地址有关的信息值类型-7
-
获取t的内存地址,并查看其内存情况
-
获取地址:
po withUnsafePointer(to: &t){print($0)}
-
查看内存情况:
x/8g 0x0000000100008158
-
值类型-8
问题:此时将t赋值给t1,如果修改了t1,t会发生改变吗?
-
直接打印t及t1,可以发现t并没有因为t1的改变而改变,主要是因为因为
t1
和t
之间是值传递
,即t1和t是不同内存空间,是直接将t
中的值拷贝至t1
中。t1
修改的内存空间,是不会影响t
的内存空间的值类型-9
SIL验证
同样的,我们也可以通过分析SIL来验证结构体是值类型
-
在
SIL
文件中,我们查看结构体的初始化方法,可以发现只有init
,而没有malloc
,在其中看不到任何关于堆区的分配值类型-10
总结
-
结构体是值类型
,且结构体的地址就是第一个成员的内存地址 -
值类型
-
在内存中直接
存储值
-
值类型的赋值,是一个
值传递
的过程,即相当于拷贝了一个副本,存入不同的内存空间,两个空间彼此间并不共享状态
-
值传递
其实就是深拷贝
-
引用类型
类
**类的常用写法 **
//****** 写法一 *******
class CJLTeacher {
var age: Int = 18
func teach(){
print("teach")
}
init(_ age: Int) {
self.age = age
}
}
var t = CJLTeacher.init(20)
//****** 写法二 *******
class CJLTeacher {
var age: Int?
func teach(){
print("teach")
}
init(_ age: Int) {
self.age = age
}
}
var t = CJLTeacher.init(20)
-
在类中,如果属性没有赋值,也不是可选项,编译会报错
引用类型-1
-
需要自己实现
init
方法
为什么类是引用类型?
定义一个类,通过一个例子来说明
class CJLTeacher1 {
var age: Int = 18
var age2: Int = 20
}
var t1 = CJLTeacher1()
类初始化的对象t1,存储在全局区
-
打印t1、t:
po t1
,从图中可以看出,t1
内存空间中存放的是地址
,t中存储的是值
引用类型-2
-
获取t1变量的地址,并查看其内存情况
-
获取
t1
指针地址:po withUnsafePointer(to: &t1){print($0)}
-
查看t1全局区地址内存情况:
x/8g 0x0000000100008218
-
查看t1地址中存储的堆区地址内存情况:
x/8g 0x00000001040088f0
-
引用类型-4
引用类型 特点
-
1、地址中存储的是
堆区地址
-
2、
堆区地址
中存储的是值
问题1:此时将t1赋值给t2,如果修改了t2,会导致t1修改吗?
-
通过
lldb
调试得知,修改了t2
,会导致t1改变
,主要是因为t2
、t1
地址中都存储的是同一个堆区地址
,如果修改,修改是同一个堆区地址,所以修改t2会导致t1一起修改,即浅拷贝
引用类型-5
问题2:如果结构体中包含类对象,此时如果修改t1中的实例对象属性,t会改变吗?
代码如下所示
class CJLTeacher1 {
var age: Int = 18
var age2: Int = 20
}
struct CJLTeacher {
var age: Int = 18
var age2: Int = 20
var teacher: CJLTeacher1 = CJLTeacher1()
}
var t = CJLTeacher()
var t1 = t
t1.teacher.age = 30
//分别打印t1和t中teacher.age,结果如下
t1.teacher.age = 30
t.teacher.age = 30
从打印结果中可以看出,如果修改t1中的实例对象属性,会导致t中实例对象属性的改变。虽然在结构体中是值传递
,但是对于teacher
,由于是引用类型
,所以传递
的依然是地址
同样可以通过lldb
调试验证
-
打印t的地址:
po withUnsafePointer(to: &t){print($0)}
-
打印t的内存情况:
x/8g 0x0000000100008238
-
打印t中teacher地址的内存情况:
x/8g 0x000000010070e4a0
引用类型-6
注意:在编写代码过程中,应该尽量避免值类型包含引用类型
查看当前的SIL
文件,尽管CJLTeacher1
是放在值类型中的,在传递的过程中,不管是传递还是赋值,teacher
都是按照引用计数
进行管理的
引用类型-7
可以通过打印teacher
的引用计数来验证我们的说法,其中teacher的引用计数为3
引用类型-8
主要是是因为:
-
main
中retain
一次 -
teacher.getter
方法中retain一次 -
teacher.setter
方法中retain一次引用类型-9
mutating
通过结构体
定义一个栈
,主要有push、pop方法,此时我们需要动态修改栈中的数组
-
如果是以下这种写法,会直接报错,原因是
值类型本身是不允许修改属性
的引用类型-10
-
将push方法改成下面的方式,查看
SIL
文件中的push
函数
struct CJLStack {
var items: [Int] = []
func push(_ item: Int){
print(item)
}
}
引用类型-11
从图中可以看出,push
函数除了item
,还有一个默认参数self
,self
是let
类型,表示不允许修改
-
尝试1:如果将push函数修改成下面这样,可以添加进去吗?
struct CJLStack {
var items: [Int] = []
func push(_ item: Int){
var s = self
s.items.append(item)
}
}
打印结果如下
可以得出上面的代码并不能将item添加进去,因为s
是另一个结构体对象,相当于值拷贝
,此时调用push
是将item
添加到s
的数组中了
-
根据前文中的错误提示,给push添加
mutating
,发现可以添加到数组了
struct CJLStack {
var items: [Int] = []
mutating func push(_ item: Int){
items.append(item)
}
}
查看其SIL文件,找到push函数,发现与之前有所不同,push
添加mutating
(只用于值类型)后,本质上是给值类型函数
添加了inout
关键字,相当于在值传递的过程中,传递
的是引用
(即地址)
inout关键字
一般情况下,在函数的声明中,默认的参数都是不可变
的,如果想要直接修改,需要给参数加上inout
关键字
-
未加
inout
关键字,给参数赋值,编译报错引用类型-14
-
添加
inout
关键字,可以给参数赋值
总结
-
1、结构体中的函数如果想修改其中的属性,需要在函数前加上
mutating
,而类则不用 -
2、
mutating
本质也是加一个inout修饰的self
-
3、
Inout
相当于取地址
,可以理解为地址传递
,即引用 -
4、
mutating
修饰方法
,而inout
修饰参数
总结
通过上述LLDB
查看结构体 & 类的内存模型,有以下总结:
-
值
类型,相当于一个本地excel
,当我们通过QQ传给你一个excel时,就相当于一个值类型,你修改了什么我们这边是不知道的 -
引用
类型,相当于一个在线表格
,当我们和你共同编辑一个在先表格时,就相当于一个引用类型,两边都会看到修改的内容 -
结构体
中函数修改属性
, 需要在函数前添加mutating
关键字,本质是给函数的默认参数self
添加了inout
关键字,将self
从let
常量改成了var
变量