值类型
前提:需要了解内存五大区,内存五大区可以参考这篇文章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变量
本文详细介绍了iOS内存的五大区,重点讨论了值类型(如结构体)和引用类型(如类)的区别。值类型在栈上存储,传递时拷贝值,而引用类型在堆上分配,传递的是引用。值类型通过`mutating`关键字允许修改属性,而引用类型直接修改会影响所有引用。此外,还探讨了`inout`关键字的作用,以及如何通过内存模型理解值类型和引用类型的行为差异。

9997

被折叠的 条评论
为什么被折叠?



