SwiftLearningNote:Struct And Class

211 阅读6分钟

在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) 就是变量b

rax常常作为函数的返回值使用
rdi,rsi,rdx.rcx,r8,r9等寄存器常常作为存放函数的参数
如果参数过多的情况下,要用到栈操作 rsp和rbp,用来存储栈空间的字节地址值

我们在看下面的一个方法:

汇编如下:

认真观察下,内存地址是连续的

注意:rip作为指令指针
存储着cpu下一条要执行指令的地址
一旦cpu读取一条指令,rip会自动指向下一条指令

接下来我们认真查看下刚才的方法的分析堆空间的汇编代码:

只要看到allcating init 就是向堆空间申请内存

这里我们拷贝rax的内存地址查看以下,如下:

所以就是:申请堆空间的对象的内存地址放到了s1指针变量的内存空间

总结下规律: