swift探索3: 内存分区&值类型&引用类型

值类型

前提:需要了解内存五大区,内存五大区可以参考这篇文章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的改变而改变,主要是因为因为t1t之间是值传递,即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改变,主要是因为t2t1地址中都存储的是 同一个堆区地址,如果修改,修改是同一个堆区地址,所以修改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
主要是是因为:

  • mainretain一次

  • 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,还有一个默认参数selfselflet类型,表示不允许修改

  • 尝试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关键字,将selflet常量改成了var变量

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值