Swift -- 04 结构体和类

696 阅读10分钟

swift.webp

结构体

在Swift标准库中,绝大多数的公开类型都是结构体,枚举和类只占很小一部分;
比如:Bool、Int、Double、String、Array、Dictionary等常见的类型都是结构体;
所有的结构体,都有一个由编译器自动生成的初始化器initializer(初始化方法、构造器、构造方法);
调用初始化器,可以传入所有成员值,用以初始化所有成员值(成员值也称:存储属性);
例如:

struct DateStruct{
    var year:Int      //存储属性,是占用内存
    var month:Int     //存储属性,是占用内存
    var day:Int       //存储属性,是占用内存
}
var date = DateStruct(year:2022,month:4,day:24) // 初始化结构体

结构体的初始化器

编译器会根据情况,可能会为结构体生成多个初始化器,主要是:保证所有成员都有初始值;\

struct Point{
    var x:Int      //并没有初始值
    var y:Int     
}
var point = Point(x:10,y:10) //如此初始化没有问题,保证了x、y都有初始值;
var point = Point(x:10)      //如此初始化系统将报错❌,只保证了x有初始值,y没有初始值;
var point = Point(y:10)      //如此初始化系统将报错❌,只保证了y有初始值,x没有初始值;
var point = Point()          //如此初始化系统将报错❌,x,y没有初始值;

所以Swift,在很多地方,都依靠编译器来保证代码的安全;

struct Point{
    var x:Int = 0      //设置初始值
    var y:Int     
}
var point = Point(x:10,y:10) //如此初始化没有问题,保证了x、y都有初始值;
var point = Point(x:10)      //如此初始化系统将报错❌,只保证了x有初始值,y没有初始值;
var point = Point(y:10)      //如此初始化没有问题,保证了y有初始值,x本身就存在初始值;
var point = Point()          //如此初始化系统将报错❌,x,y没有初始值;

所以,Swift可以生成多个初始化器

自定义初始化器

一旦在定义结构体时,自定义了初始化器,编译器就不会再帮它自动生成其他初始化器;

struct Point{
    var x:Int = 0
    var y:Int = 0
    init(x:Int,y:Int){
        self.x = x
        self.y = y
    }
}
var point = Point(x:10,y:10) //如此初始化没有问题,调用了自定义初始化器,保证了x、y都有初始值;
var point = Point(x:10)      //如此初始化系统将报错❌,虽然x、y都有初始值,但是由于自定义了初始化器,系统将不再自动生成其他初始化器
var point = Point(y:10)      //如此初始化系统将报错❌,原因同上
var point = Point()          //如此初始化系统将报错❌,原因同上

结构体内存

结构体内存大小,是由结构体存储属性的数量来决定的;

struct Point{
    var x:Int = 0
    var y:Int = 0
    var po:Bool = false
}
print(MemoryLayout<Point>.size);//实际占用 17 字节
print(MemoryLayout<Point>.stride);//系统分配 24 字节
print(MemoryLayout<Point>.alignment);//内存对齐方式 8 字节

类的定义使用关键字class声明;
类的定义和结构体相似,但编译器并没有为类自动生成可以传入成员值的初始化器;
如果类的成员在定义的时候,都指定初始值,那么编译器会为类自动生成无参的初始化器,并且成员的初始化,就是在这个初始化器中完成的;

class Point{
    var x:Int = 0
    var y:Int = 0
}
var point = Point()          //成员指定初始值,编译器自动生成无参初始化器
var point = Point(x:10,y:10) //错误❌,没有声明带参数的初始化器;
var point = Point(x:10)      //错误❌,没有声明带参数的初始化器;
var point = Point(y:10)      //错误❌,没有声明带参数的初始化器;
class Point{ //❗️如此定义class,编译器会提示错误,成员没有指定初始值
    var x:Int
    var y:Int
}
var point = Point()          //错误❌,没有声明无参的初始化器;
// 成员没有指定初始值,编译器不会自动生成无参初始化器

结构体与类的区别

1、结构体是值类型(枚举也是值类型),类是引用类型(也称指针类型)

例子:
定义一个结构体,一个类,在函数中调用;

iShot2022-04-25_12.31.02.jpg

函数中定义的变量 size 、变量 point,它们的内存栈空间;(注意⚠️:是变量的内存在栈空间)

Point 结构体内部有x、y 两个成员,都是Int类型,每个成员占8字节;
所以在栈空间里,会分配两块连续的内存空间,分别存放Point结构体的x、y成员变量对应的值(3、4);

Size 是类,也就是指针类型,说白了,var size 其实是指针变量;指针变量在64位环境里,占用8个字节;
所以size这个指针变量的内存,在栈空间里,占用8个字节,内存里存放的是size对象的内存地址

iShot2022-04-25_12.48.49.jpg

size 对象存储在堆空间,所占内存是32字节;
首地址的8字节,存放指向类型信息的内存地址;
往下8字节,存放引用计数
最后16字节,分别存放成员,比如说width、height;

iShot2022-04-25_15.11.13.jpg


验证存储空间

1、检验是否 Point()、Size() 是在栈空间、还是堆空间

func testClassAndStruct(){
    struct Point{
        var x:Int = 3
        var y:Int = 4
    }
    class Size{
        var width = 1
        var height = 1
    }
//    var size = Size()
    var point = Point()
}
testClassAndStruct()

iOS中,向堆空间申请内存空间,关键字有:allocmalloc,如果使用此类关键字,基本上都是在申请堆空间内存;

1、查看Point()是否申请堆空间

执行Point(),通过汇编,查看到底层默认会调用init()函数;

iShot2022-04-25_15.18.59.jpg

通过si指令,进入这个函数,查看内部是是否向堆空间申请内存;

iShot2022-04-25_15.25.26.jpg

而在 callq指令外层,也没有看到申请堆空间的指令;

所以,可以验证Point()是在栈空间;

2、查看Size()是否申请堆空间

执行Size(),查看汇编底层;

iShot2022-04-25_15.36.08.jpg

使用si指令,进入__allocationg_init()这个函数,查看底层到底是否分配堆内存空间

所以,对象的堆空间申请过程 如下:
1、调用 Class.__allocating_init()
2、libswiftCode.dylib:_swift_allocObject_
3、libswiftCode.dylib:_swift_slowAlloc
4、libsystem_malloc.dylib:malloc
提示:在Mac、iOS中,malloc函数分配的内存大小总是16的倍数;
比如:

    class Point{
        var x = 11  //Int类型,占用 8 字节
        var test = true   //Bool类型,占用 1 字节
        var y = 8  //Int类型,占用 8 字节
    }
    那么整个class,实际占用内存为 17字节,
    但是我们上面说过,一个类,它的前16位,主要用来存放类信息的,接着才会存放其他数据;
    所以整个类,实际占用 33 字节;
    按照内存对齐原则,系统会分配 40个字节给class;
    
    但是,由于malloc的分配原则是按照 16 的倍数来分配,所以 malloc 之后,class实际会分配 48 字节
    

总结:
1、类的实例,分配的是堆空间内存;
2、结构体不需要分配堆空间内存;
3、malloc 的分配原则是按照 16的倍数来分配;

2、值类型

1、值类型赋值给var、let或传给函数参数,是直接将所有的内容拷贝一份;
2、类似与进行copy、paste操作,会产生全新的副本,属于深拷贝
比如:

struct Point{
        var x:Int
        var y:Int
    }
    var p1 = Point(x:10,y:10)
    var p2 = point1

他们的空间布局如下:

iShot_2022-04-30_15.57.33.jpg 3、在Swift标准库中,为了提升性能,String、Array、Dictionary、Set采取了Copy on Write的技术;
也就是说,只有存在入操作时,才会真正执行拷贝操作;
比如:

var s1 = "QLY"
var s2 = s1

将 s1 赋值给 s2,但是s2s1没有对数据进行任何 写入 操作,也就是没有任何增删改操作;
所以 s1s2的数据("QLY")是同一片内存地址;


s2 = "LL"
当 s2 或 s1 有增删改操作时,他们的数据才会生成新的地址;

4、对于标准库的赋值操作,Swift能确保最佳性能,所以我们自己没有必要为了最佳性能来避免赋值;
5、建议:不需要修改的,尽量使用let定义

3、引用类型

1、引用赋值给var、let或者传递给函数参数,都是将内存地址拷贝一份;
2、类似与制作文件替身,指向的是同一个文件,属于浅拷贝
比如说:

class Size{
    var width:Int 
    var height:Int
    init(width:Int, height:Int){
        self.width = width
        self.height = height
    }
}
var s1 = Size(width:10,height:20)
var s2 = s1

如果进行如下修改:
s2.widht = 11
s2.height = 22

s2对内存数据进行修改,s1的值,也会跟着一起修改,因为他们指向同一片内存数据;

s1、s2的指向同一片内存数据;

iShot_2022-04-30_15.59.10.jpg

引用类型赋值操作

class Size{
    var width:Int 
    var height:Int
    init(width:Int, height:Int){
        self.width = width
        self.height = height
    }
}

// 假设以下两句代码是在函数中执行;
var s1 = Size(width:10,height:20)
s1 = Size(width:20,height:30)

假设s1是在函数中初始化,s1第一次赋值、初始化对象,得到的数据如下图:
s1在栈空间,它的内存数据是Size对象的地址,指向堆空间的Size对象;

iShot_2022-05-17_15.04.06.jpg

s1第二次赋值,会重新生成在堆空间分配一片内存,存储Size对象,并且将新Size对象的地址,赋值给s1, 将之前的s1内存地址覆盖,原先的Size对象,将会被系统回收;

最终的效果是:

iShot_2022-05-17_15.12.32.jpg

4、值类型、引用类型的let(常量)

首先声明一个结构体和class;

iShot_2022-05-18_10.15.39.jpg

调用问题:

iShot_2022-05-18_10.38.46.jpg

值类型与引用类型的调用区别原因:

let 是常量关键字;

所以p、size都是常量;意味着p、size的内存不可以修改
使用let修饰 p,p是结构体,所以p的内存分别是x、y对应的值;
修改p的x、y成员,意味着修改内存,常量类型内存不可修改,所以提示错误;

使用let修饰 size,size是对象,size的内存存储的是size对象的地址;
修改size对象,意味着修改size对象的内存地址,常量类型内存不可修改,所以提示错误;

但是:
size.width = 33
size.heigth = 44
这两句代码,并是不修改size的内存地址,改的是堆空间里,size对象里的某块内存地址;所以不会提示错误

5、嵌套类型

定义:

struct Frame {    
    enum Point : Int {
        case x = 1,y
    }

    enum Size :Int {
        case width = 10,height
    }
}

在结构体Frame内存,定义两个枚举;

使用:

var x = Frame.Point.x

var width = Frame.Size.width

6、枚举、结构体、类内部都可以定义方法

含义:一般把定义在枚举、结构体、类内部的函数,叫方法; 例子

class Size{
    var width:Int 
    var height:Int
    func show(){
        print("show")
    }
}

调用:
let size = Size()
size.show()

将函数定义在枚举、结构体、类内部,本质上与把函数定义在外部,没有区别;

例如:
func show(self:Size){
    print("show")
}

此处的show函数,与上面的Size类内部的show函数,没有区别;

调用:
let size = Size()
show(size)

那么方法是否占用对象内存?

  • 方法是不占用对象内存的;
  • 方法的本质是函数,函数存放在代码段,所以方法也存放在代码段

为什么不存放在对象内存里呢?

  • 如果存放在对象内存里,当初始化多个对象时,对象里的方法,都是同一套逻辑,就会导致多个对象,同一波代码有占用多个内存,造成内存浪费;
例如:

调用:
let size = Size()
size.show()

let size1 = Size()
size1.show()

let size2 = Size()
size2.show()

size、size1、size2,都是Size对象,如果show方法存放在对象里,
那么每一个对象,都需要开辟一段内存来存储show方法,若如此就内存浪费;