在swift标准库中,绝大多数是结构体,只有少部分是类和枚举。
结构体
比如:bool,string,int,float,double,array,dictionary 等常见都是结构体,
举个🌰:
struct NewDate{
var year: Int
var month: Int
var day: Int
}
var date = NewDate(year: 2019, month: 12, day: 18)
以上就是一个很简单的结构体的例子,需要值得小伙伴们注意的是,其实在swift的语法中,所有的结构体都有一个编译器自动生成的初始化
器(initializer,初始化方法,构造方法,构造器)类似我们oc中的init
结构体的初始化器
编译器会根据具体情况,可能会为结构体生成多个初始化器,宗旨:保证所有的成员都有初始值。 这里存在多种初始化器的方式:
struct Point1{
var x:Int
var y:Int
}
var p10 = Point1(x:1,y:2)
var p20 = Point1(x:1)
var p30 = Point1(y:2)
struct Point2{
var x:Int = 0
var y:Int
}
var p11 = Point2(x:1,y:2)
var p12 = Point2(x:1)
var p13 = Point2(y:2)
显然,在Point1方法中只有 p10是对的,在Point2方法中p11和p13是对的,根据构造方法的方法参数的个数和默认值确定了 其他的都会报错。 最好的方式和习惯就是尽量在初始化的时候 成员变量给定默认值 如图:
下面大家来思考一个问题 下面的代码能否编译通过呢 :
struct Point{
var x:Int ?
var y:Int ?
}
var p1 = Point(x: 10, y: 10)
var p2 = Point(x: 11)
var p3 = Point(y: 12)
var p4 = Point()
可选项都是有个默认值nil
因此可以编译通过
自定义初始化器
struct Point{
var x:Int = 0
var y:Int = 0
init(x:Int,y:Int)
self.x = x
self.y = y
}
var p1 = Point(x: 10, y: 10)
var p2 = Point(x: 11)
var p3 = Point(y: 12)
var p4 = Point()
init方法就是 自定义初始化器 这里p2,p3,p4都会报错,就是自己自定义了自己的初始化器,系统就不会帮你再次定义了
窥探初始化器的本质
以下的两段代码是等效的
struct Point{
var x:Int = 0
var y:Int = 0
}
var p1 = Point()
struct Point{
var x:Int
var y:Int
init() {
x = 0
y = 0
}
}
var p2 = Point()
类
类的定义和结构体类似,但是编译器并没有为类自动生成可以传入的成员值初始化器
这里class类中的成员变量值给它赋值是(x=0,y=0),表面上看是在类里面赋值,实际上是在 p1 = Point() 初始化构造器中生成的
如果类的所有成员变量在定义的时候就已经给了初始值,编译器会给类生成无参的初始化器
class Point{
var x:Int = 10
var y:Int = 20
}
let p1 = Point()
// 这两段代码完全等效
class Point{
var x:Int
var y:Int
init() {
x = 10
y = 20
}
}
var p2 = Point()
在swift中,结构体和类里面都是可以定义方法func
结构体和类的本质区别
结构体是值类型(枚举也是值类型),类是引用类型(指针类型)
一进来打上断点的时候就出现这个:si 进去以后看看汇编: 其实有个技巧就是给已知参数中,上面的参数10和20,11和22
通过这里面就可以判断10,20,11,22 ,就是为了将来初始化p1的,就是为了初始化init
其实在上一篇的文章里面edi,和esi的内存其实在rdi和rsi里面,那么这个时候我们进去init初始化的方法里面看看:
rdi应该就是10,rsi就是20,这两个值又分别给rax和rdx这里发现将10和20 分别存储在rbp-0x10的地址和rbp-0x8的地址上,再看下一行,发现rax和rdx又放到另外的地址去了
将刚才的地址复制出来发现:
将rax和rdx放到了连续的16个字节中以上的汇编代码就是对应了以下的代码
全局变量看汇编
下面我变化以下,将刚才的变量放到函数方法的外面去看看汇编,内存应该会是有点变化的
不懂汇编就直接看字面量:
同样20给了rax,给了rsi,同时调用init方法,call函数里面去看看这就跟刚才init的方法差不多,同样将10和20放到对应的寄存器中去,这时候敲finish,函数跳出来
发现了rax和rdx给了这两个家伙
这时候我们会发现,rip会是执行下一句的地址
可以看出:rax和rdx也是放到连续的16个字节中不管是在全局区还是栈空间,它这个是结构体都是内容拷贝
以后的经验之谈:
以后看到rbp-0xxx 一般都是局部变量
以后看到是 rip+0xxx 一个很大的数 就是全局变量
注意:这个时候的p1,p2内存是固定了,放在全局区域
如果放在局部变量,每次都是可变的
这里的地址rip地址是固定的,每次进来的 main函数都是这种 每次程序员启动,这些地址放在代码去,估计写死的这个时候我再次拷贝到函数中,变成局部变量 rbp每次调用函数都用可能不一样哦
mov rsp rbp 这里的rsp是又外层函数决定的
引用类型
1 引用赋值给var,let或者给函数传参,是将内存地址拷贝一份 2 类似制作一个文件的替身,指向的是同一个文件,属于浅拷贝
s1,s2都是指针变量,每一个指针变量都是占用8个字节,所以总的16个字节
实际上是s1里面的8个地址数据传给s2
分析引用类型的汇编代码
这时候是s2的堆空间对应的内容改成11,22这个汇编分析就有点难度了,这里没有之前的全局变量和局部变量 做 堆空间的数据分析的时候需要知道一些基础的常识
如上比如写一个简单的函数:
func get() -> Int {
return 10
}
var b = get()
开始汇编走去:
这样 0x5415(%rip) 就是变量brax常常作为函数的返回值使用
rdi,rsi,rdx.rcx,r8,r9等寄存器常常作为存放函数的参数
如果参数过多的情况下,要用到栈操作 rsp和rbp,用来存储栈空间的字节地址值
我们在看下面的一个方法:
汇编如下:认真观察下,内存地址是连续的
注意:rip作为指令指针
存储着cpu下一条要执行指令的地址
一旦cpu读取一条指令,rip会自动指向下一条指令
接下来我们认真查看下刚才的方法的分析堆空间的汇编代码:
只要看到allcating init 就是向堆空间申请内存
这里我们拷贝rax的内存地址查看以下,如下:
所以就是:申请堆空间的对象的内存地址放到了s1指针变量的内存空间
总结下规律: