在Swift标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分。
一、结构体
常见的Bool、Int、Double、String、Array、Dictionary等常见类型都是结构体。
自定义结构体:
代码语言:javascript复制struct Date {
var year: Int;
var month: Int;
var day: Int;
}
var date = Date(year: 2019, month: 06, day: 02)
所有结构体都有一个编译器自动生成的初始化器(initializer、初始化方法、构造器、构造方法)。
Date(year: 2019, month: 06, day: 02)
传入的是所有成员值,用来初始化所有成员(叫做存储属性)。
1.1. 结构体的初始化器
编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员都有初始值。
从上面案例可以看出,编译器帮助生成初始化器的条件就是:让所有存储属性都有值。
> 思考:下面的代码能否编译通过?
可选项都有个默认值nil
,所以可以编译通过。
1.2. 自定义初始化器
一旦在定义结构体时自定义了初始化器,编译器就不会再帮它自动生成其他初始化器。
1.3. 探究结构体初始化器的本质
下面的两段代码是等效的:
代码一:
代码二:
经过对比发现,代码一和代码二的init
方法完全一样。也就是说,存储属性的初始化是在初始化构造方法中完成的。
1.4. 结构体的内存结构
代码语言:javascript复制struct Point {
var x = 10
var y = 20
var b = true
}
var p = Point()
print(Mems.memStr(ofVal: &p))
print(MemoryLayout<point>.size)
print(MemoryLayout<point>.stride)
print(MemoryLayout<point>.alignment)
/*
输出:
0x000000000000000a 0x0000000000000014 0x0000000000000001
17
24
8
*/
因为存储属性x
和y
各占8个字节(连续内存地址),Bool
在内存中占用1个字节,所以Point
一共占用17个字节,由于内存对齐是8,所以一共分配了24个字节。
二、类
类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器。
定义类:
如果存储属性没有初始值,无参的初始化器也不会自动生成:
如果把上面的类换成结构体(struct
)类型就不会报错:
2.1. 类的初始化器
如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器。
成员的初始化是在这个初始化器中完成的。
下面的两段代码是等效的: 代码一:
代码语言:javascript复制class Point {
var x: Int = 0
var y: Int = 0
}
var p1 = Point()
代码二:
代码语言:javascript复制class Point {
var x: Int
var y: Int
init() {
self.x = 0
self.y = 0
}
}
var p1 = Point()
三、结构体与类的本质区别
结构体时值类型(枚举也是值类型),类是引用类型(指针类型)。
3.1. 内存分析结构体与类
示例代码:
代码语言:javascript复制class Size {
var width: Int = 1
var height: Int = 2
}
struct Point {
var x: Int = 3
var y: Int = 4
}
func test() {
var size = Size()
print("class-size对象的内存",Mems.memStr(ofRef: size))
print("class-size指针的内存地址",Mems.ptr(ofVal: &size))
print("class-size对象的内存地址",Mems.ptr(ofRef: size))
print("class-size.width的内存地址",Mems.ptr(ofVal: &size.width))
print("class-size.height的内存地址",Mems.ptr(ofVal: &size.height))
var point = Point()
print("struct-point对象的内存",Mems.memStr(ofVal: &point))
print("struct-point的内存地址",Mems.ptr(ofVal: &point))
print("struct-point.x的内存地址",Mems.ptr(ofVal: &point.x))
print("struct-point.y的内存地址",Mems.ptr(ofVal: &point.y))
}
test()
/*
输出:
class-size对象的内存 0x00000001000092a8 0x0000000200000002 0x0000000000000001 0x0000000000000002
class-size指针的内存地址 0x00007ffeefbff4d0
class-size对象的内存地址 0x000000010061fe80
class-size.width的内存地址 0x000000010061fe90
class-size.height的内存地址 0x000000010061fe98
struct-point对象的内存 0x0000000000000003 0x0000000000000004
struct-point的内存地址 0x00007ffeefbff470
struct-point.x的内存地址 0x00007ffeefbff470
struct-point.y的内存地址 0x00007ffeefbff478
*/
示例代码的在内存中:
经过分析可以看到,结构体的数据是直接存到栈空间的,类的实例是用指针指向堆空间的内存,指针在栈空间。上面示例代码中类的实例占用32个字节,其中前面16个字节分别存储指向类型信息和引用计数,后面16个字节才是真正用来存储数据的。而结构体占用的内存大小等于存储属性所占内存大小之和。
> 注意:在C
语言中,结构体是不能定义方法的,但是在C
和Swift
中,可以在结构体和类中定义方法。在64bit环境中,指针占用8个字节。
>> 扩展:值类型(结构体、枚举)的内存根据所处的位置不同,内存的位置也不一样。例如,定义一个全局的结构体,内存在数据段(全局区)中;如果在函数中定义,内存存放在栈空间;如果在类中定义一个结构体,内存跟随对象在堆空间。
3.2. 汇编分析结构体与类
在Swift
中,创建类的实例对象,要向堆空间申请内存,大概流程如下:
Class.__allocating_init()
libswiftCore.dylib:_swift_allocObject_
libswiftCore.dylib:swift_slowAlloc
libsystem_malloc.dylib:malloc
在Mac,iOS中的malloc
函数分配的内存大小总是16的倍数(为了做内存优化)。
通过class_getInstanceSize
可以得知类的对象真正使用的内存大小。
import Foundation
class Point {
var x: Int = 3
var y: Int = 4
var b: Bool = true
}
var p = Point()
print(class_getInstanceSize(type(of: p)))
print(class_getInstanceSize(Point.self))
/*
输出:
40
40
*/
内存占用大小 = 8(指向类型信息) 8(引用计数) 8(存储属性x) 8(存储属性y) 1(存储属性b) = 33;
内存分配大小 = 8(指向类型信息) 8(引用计数) 8(存储属性x) 8(存储属性y) Max(1(存储属性b), 8(内存对齐数)) = 40;
> 扩展:如果底层调用了alloc或malloc函数,说明该对象存在堆空间,否则就是在栈空间。
3.2.1. 汇编分析结构体
第一步:创建结构体,打断点进入汇编:
第二步:在callq...init()
函数处进入函数实现体(lldb进入函数体指令:si
):
结论:rbp
就是局部变量,所以结构体创建的对象是在栈中存储的。
> 扩展:一般情况下,rbp
就是局部变量,rip
是全局变量,ret
是函数返回。
3.2.2. 汇编分析类
第一步:创建结构体,打断点进入汇编:
第二步:在callq...__allocating_init()...
函数处打断点,进入函数体:
第三步:在callq...swift_allocObject
函数处打断点,进入函数体:
第四步:一直进入到libswiftCore.dylib swift_allocObject:
中,在callq...swift_slowAlloc
处打断点进入:
第五步:malloc
出现了,这时候继续进入函数体:
第六步:最终,对象是在libsystem_malloc.dylib
库中执行的malloc
:
经过上面分析,可以清晰的看到,对象是在堆空间存储的。
扩展:在Mac、iOS中,创建对象都是调用的
libsystem_malloc.dylib
动态库。