值类型:类似于本地的Excel,修改的内容只有自己知道。
引用类型:类似于在线的表格,修改的内容大家都知道。
在我们剖析值类型和引用类型之前,我们向来回顾一下iOS的内存分区。

内存分区.png
-
栈区的地址 比堆区的地址大 -
栈是从高地址->低地址,向下延伸,由系统自动管理,是一片连续的内存空间 -
堆是从低地址->高地址,向上延伸,由程序员管理,堆的空间结构类似于链表,不是连续的 - 日常开发中的
内存溢出是指堆栈溢出,可以理解为栈区与堆区边界碰撞的情况 -
全局区、常量区都存储在Mach-o中的_ _TEXT cString段t
接下里我们详细探讨一下值类型 与 引用类型
值类型
我们来看下面的例子:
func text() {
var size = 10
var size_2 = size
size = 20
print("第一次改变")
print("size=\(size), size_2=\(size_2)")
siz_2 = 30
print("第二次改变")
print("size=\(size), size_2=\(size_2)")
}
text()
/************** 输出结果 **************/
第一次改变
size=20, size_2=10
第二次改变
size=20, size_2=30
Program ended with exit code: 0
- 从输出结果来看,
size和size_2符合 本地Excel 的特点,两个对象各自修改自己的值,对方都不知情。初步判定:size和size_2是值类型。
我们接着往下看,现在我们用LLDB来查看一下size和size_2的内存结构

image.png
- 从上图中,我们可以看出来,
size和size_2的内存地址符合栈内存的情况,是连续的(且是从高到底的),相差了8个字节。而这8个字节正好是一个Int。
我们再来看一张图:

image1.png
- 这一次我们总共设置了两个断点,通过两次LLDB调试,我们不仅能从内存地址的大小来断定
size和size_2是两个连续的内存地址,而且,从值变化也可以清晰的分辨出来。
总结:
值类型的特点:
1、值类型存储在栈内
2、地址中存储的就是值
3、值类型的传递过程中会产生新的副本,是深拷贝
4、值类型赋值给var、let或者给函数传参,是直接将所有内容拷贝一份
注意:
- 在
Swift标准库中,为了提升性能,String、Array、Dictionary、Set采取了Copy On Write的技术。- 比如仅当有 “写” 操作时,才会真正执行拷贝操作
- 对于标准库值类型的赋值操作,Swift能确保最佳性能,所以没必要为了保证最佳性能来避免赋值
- 注意:Copy On Write 只对Swift标准库起作用
- 建议:不需要修改的,尽量定义成
let
结构体
- 在Swift标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分。
- 比如:
Bool、Int、Double、String、Array、Dictionary等常见的类型都是结构体
- 比如:
struct Date {
var year: Int
var month: Int
var day: Int
}
var date = Date(year: 2020, month: 12, day: 17)
- 所有的结构体都有一个编译器自动生成的初始化器(initializer、初始化方法、构造器、构造方法)
- 编译器会根据情况,可能会为结构体生成多个初始化器,
image2.png -
如果将结构体内的属性定义成可选类型会怎样呢?
- 通过下图可以看到,编译器并不会报错
- 因为
可选项都有一个默认值nil,因此可以编译通过
image3.png
-
当然,我们也可以自定义初始化器。
-
一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器,如下图所示:
image4.png
-
- 接下来我们通过汇编来窥探一下初始化器的本质
- 下面给出两个结构体,通过汇编来看一下
init
- 下面给出两个结构体,通过汇编来看一下
struct Date {
var year: Int
var month: Int
init() {
year = 2020
month = 12
}
}
var date = Date()
struct Date {
var year: Int = 2020
var month: Int = 12
}
var date = Date()
1、首先我们来看第一个结构体(进行断点调试):

image5.png

image6.png
2、我们再来看一下第二个结构体(进行断点调试):

image7.png

image8.png

image9.png
- 通过对比上面连个结构体进入的
init()方法,我们发现,最后进入的init()方法是一模一样的,没有任何区别。 - 因此可以得出结论,结构体的
初始化器无论是编译器默认的还是自定义的,其实没有本质区别。 - 结构体的内存结构
struct Date {
var year = 2020
var month = 12
var Good = true
}
print(MemoryLayout<Date>.size)
print(MemoryLayout<Date>.stride)
print((MemoryLayout<Date>.alignment))
/********** 输出结果 ************/
17
24
8
- 当然,此时我们也可以用
withUnsafePointer函数去查看结构体的内存地址。 - 通过内存结构的查看,我们发现,
结构体也是值类型
总结:
1、结构体是值类型,且结构体的地址是第一个成员的内存地址(可通过LLDB查看)
2、结构体的默认初始化器和自定义初始化器没有任何区别
3、结构体一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器
4、所有的结构体都有一个编译器自动生成的初始化器
5、编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员都有初始值
引用类型
类
eg:
class Man {
var age = 30
var height: Int?
init(_ age: Int) {
self.age = age
}
init(height: Int) {
self.height = height
}
init(_ age: Int, _ height: Int) {
self.age = age
self.height = height
}
}
let M1 = Man.init(20)
let M2 = Man.init(height: 180)
let M3 = Man.init(18, 190)
- 在类中,如果属性没有赋值,且为非可选项,此时需要自定义
init方法
为什么类是引用类型?
eg:
class Man {
var age = 30
var height: Int?
}
let M1 = Man.init()
下面我们断点调试一下M1

image.png
- 通过上图可以看到,
M1里面存放的是地址。
接下来我们通过值的修改来观察一下引用类型的变化(注意:上文中M1是let类型,接下来我们需要用var类型)
image.png - 通过上图我们可以看到,
M1和M2里面存放的地址是一样的,因此var M2 = M1是地址拷贝,即浅拷贝。并且我们可以观察到M2.age的改变也影响到了M1.age的值。
接下来我们思考一个问题
通过上文我们知道:
-
结构体是值类型 -
类是引用类型
那么结构体与类的结合会影响到对方的存储形式吗?
image.png - 通过上图,我们可以看到
p中的M1仍然存放的是地址,由此可见结构体中包含类对象并不会改变其存储形式。 - 反过来也是一样的,
类包含结构体也不会形象结构体的存储形式,有兴趣的同学可以自己尝试一样,过程跟上面的一样。
总结:
1、类是引用类型。
2、类对象里面存储的是地址,类对象之间的赋值属于地址传递(浅拷贝)。
3、值类型与引用类型互相包含的情况下,并不会改变各自的存储形式。




