美文网首页
iOS-Swift-结构体和类

iOS-Swift-结构体和类

作者: Imkata | 来源:发表于2020-01-06 22:17 被阅读0次

    一. 结构体

    1. 结构体简介

    在 Swift 标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分,比如Bool、Int、Double、 String、Array、Dictionary等常见类型都是结构体

    结构体.png

    所有的结构体都有一个编译器自动生成的初始化器(initializer,初始化方法、构造器、构造方法)
    在第⑥行调用的,可以传入所有成员值,用以初始化所有成员(存储属性,Stored Property)

    枚举:枚举可以使⽤rawValue来给枚举赋值,没有自动生成的初始化器
    结构体:所有的结构体都有编译器⾃动⽣成的初始化器(也许不⽌⼀个),⽤于初始化所有成员,但是如果你⾃定义了初始化器,编译器就不会帮你了
    类:编译器没有为类⾃动⽣成可以传⼊成员值的初始化器(想让我们⾃⼰写),但是如果属性都有默认值,也会帮我们创建⼀个⽆参初始化器

    当然,枚举、结构体、类都可以自定义初始化器

    2. 结构体的初始化器

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

    结构体的初始化器.png

    如果枚举的某个成员有默认值,初始化器初始化的时候可以不给这个成员设置值。反之,如果没默认值,那么一定要设置值。

    • 思考:下面代码能编译通过吗?
    思考

    可选项都有个默认值nil,因此可以编译通过

    3. 自定义初始化器

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

    自定义初始化器.png

    如上,自定义了初始化器,那么编译器就不会帮你自动生成任何其他初始化器。

    4. 设置初始值的本质

    结构体设置初始值,就相当于定义了一个无参初始化器

    初始值的本质.png

    以上两段代码完全等效。

    下面通过窥探汇编,验证上面代码完全等效:

    1. 执行如下代码:
    func test() {
        struct Point {
            var x: Int
            var y: Int
            init() {
                x = 0
                y = 0
            }
        }
        
        var p = Point() //打断点
    }
    
    test()
    

    断点暂停在下面这行,可以看出这行代码就是函数调用。

    ->  0x10000245f <+15>: callq  0x100002480               ; init() -> Point #1 in TestSwift.test() -> () in Point #1 in TestSwift.test() -> () at main.swift:15
    

    敲si,进入函数

    (lldb) si
    
    TestSwift`init() in Point #1 in test():
    ->  0x100002480 <+0>:  pushq  %rbp
    0x100002481 <+1>:  movq   %rsp, %rbp
    0x100002484 <+4>:  xorps  %xmm0, %xmm0
    0x100002487 <+7>:  movaps %xmm0, -0x10(%rbp)
    0x10000248b <+11>: movq   $0x0, -0x10(%rbp)  //将0赋值给x
    0x100002493 <+19>: movq   $0x0, -0x8(%rbp)   //将0赋值给y
    0x10000249b <+27>: xorl   %eax, %eax
    0x10000249d <+29>: movl   %eax, %ecx
    0x10000249f <+31>: movq   %rcx, %rax
    0x1000024a2 <+34>: movq   %rcx, %rdx
    0x1000024a5 <+37>: popq   %rbp
    0x1000024a6 <+38>: retq
    

    调用了init()函数。

    1. 将上面代码改成:
    func test() {
        struct Point {
            var x: Int = 0
            var y: Int = 0
        }
        
        var p = Point() //打断点
    }
    
    test()
    

    上面的操作再来一遍,结果如下:

    ->  0x10000245f <+15>: callq  0x100002480               ; init() -> Point #1 in TestSwift.test() -> () in Point #1 in TestSwift.test() -> () at main.swift:15
    
    TestSwift`init() in Point #1 in test():
    ->  0x100002480 <+0>:  pushq  %rbp
    0x100002481 <+1>:  movq   %rsp, %rbp
    0x100002484 <+4>:  xorps  %xmm0, %xmm0
    0x100002487 <+7>:  movaps %xmm0, -0x10(%rbp)
    0x10000248b <+11>: movq   $0x0, -0x10(%rbp)  //将0给x
    0x100002493 <+19>: movq   $0x0, -0x8(%rbp)   //将0给y
    0x10000249b <+27>: xorl   %eax, %eax
    0x10000249d <+29>: movl   %eax, %ecx
    0x10000249f <+31>: movq   %rcx, %rax
    0x1000024a2 <+34>: movq   %rcx, %rdx
    0x1000024a5 <+37>: popq   %rbp
    0x1000024a6 <+38>: retq
    

    可以发现,两次生成的汇编代码一模一样,验证了:结构体设置初始值,就相当于定义了一个无参初始化器

    5. 结构体内存结构

    结构体内存结构

    执行如下代码:

    func testStruct() {
        struct Point {
            var x = 10
            var y = 20
            var b = true
        }
        var p = Point(x: 11, y: 22, b: false)
        print(Mems.memStr(ofVal: &p))
        
        print(MemoryLayout<Point>.size)
        print(MemoryLayout<Point>.stride)
        print(MemoryLayout<Point>.alignment)
    }
    
    testStruct()
    

    打印:

    0x000000000000000b 0x0000000000000016 0x0000000000000000
    17
    24
    8
    

    可以看出前8字节存放10,后8字节存放20,最后1个字节存放0
    实际需要8 + 8 + 1 = 17字节,由于内存对齐是8,所以系统分配24字节,和我们想的一样。

    二. 类

    1. 类简介

    类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器

    没有自动生成初始化器

    2. 类的初始化器

    如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器,成员的初始化是在这个初始化器中完成的

    初始值的本质.png

    以上两段代码完全等效,上面已经验证过了,这里就不验证了。

    三. 结构体与类的区别

    1. 本质区别

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

    struct Point {
     var x = 3
     var y = 4
    }
    
    class Size {
     var width = 1
     var height = 2
    }
    
    //创建结构体和类
    func test() {
     var point = Point() 
     var size = Size()
    }
    

    运行如上代码,创建结构体和类之后,内存分布如下:

    内存分布(64bit环境).png

    结构体和指针在栈空间,栈空间存放的指针指向的是对象的地址值,系统⾃⼰管理。(上面的point结构体占用16字节,前8字节放3,后8字节放4)。
    对象和闭包在堆空间,我们⾃⼰管理,不过ARC模式下,编译器会⾃动帮我们加release。(上面的size对象占用32字节,每8字节分别存放指向类型信息、引用计数、1、2)。

    2. 值类型

    值类型赋值给var、let或者给函数传参,是直接将所有内容拷贝一份,类似于对文件进行copy、paste操作,产生了全新的文件副本,属于深拷贝(deep copy)

    如下代码:

    代码.png

    内存中分布如下:

    内存.png

    问:执行如下代码之后,p1.x和p1.y是多少?

    p2.x = 11
    p2.y = 22 
    

    答:因为是值传递,所以修改p2不会影响p1,所以p1:(10,20)、p2:(11,22)

    • Copy On Write
    1. 在Swift标准库中,为了提升性能,String、Array、Dictionary、Set采取了Copy On Write的技术,比如仅当有“写”操作时,才会真正执行拷贝操作,否则都是浅拷贝(就是只拷贝指针)。
    2. 对于标准库值类型的赋值操作,Swift能确保最佳性能,所有没必要为了保证最佳性能来避免赋值
    3. 建议不需要修改的,尽量定义成let
    • 值类型的赋值操作
    值类型赋值.png 值类型赋值内存变化.png

    3. 引用类型

    引用赋值给var、let或者给函数传参,是将内存地址拷贝一份,类似于制作一个文件的替身(快捷方式、链接),指向的是同一个文件。属于浅拷贝(shallow copy)

    引用类型.png

    结果如下,由于是引用类型,所以修改s2,s1指向的内容也被修改了。

    s1.width==s2.width==10
    s2.height==s2.height==20
    

    内存情况如下:

    内存情况
    • 引用类型的赋值操作
    引用类型的赋值操作.png

    4. 值类型、引用类型的let

    值类型、引用类型的let.png

    可以看出:值类型的let,什么都不能改。引⽤类型的let,不能指向其他对象 (保存的指针地址不能变)。

    四. 对象的堆空间申请过程

    上面说了,结构体和指针在栈空间,对象和闭包在堆空间,这里验证一下:

    内存区域、Tagged Pointer里面说过,通过alloc、malloc、calloc等动态分配的空间在堆里面,所以我们只需要证明创建对象后有调用alloc、malloc方法就可以。

    运行代码:

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

    对于结构体来说,汇编函数调用比较简单,这里就省略了,并没有调用alloc、malloc等方法,说明结构体不在堆空间,在栈空间。

    重新运行上面代码,敲si指令,一步一步查看汇编代码,根据如下流程:

    在Swift中,创建类的实例对象,要向堆空间申请内存,大概流程如下:
    Class.__allocating_init()
    libswiftCore.dylib:_swift_allocObject_
    libswiftCore.dylib:swift_slowAlloc
    libsystem_malloc.dylib:malloc
    

    最后调用了malloc,说明对象的确在堆空间。

    注意:

    1. MemoryLayout用来查看枚举和结构体内存使⽤
    2. class_getInstanceSize用来查看实例对象,至少占⽤多少内存(内存对齐后的)
    3. malloc_size用来查看实例对象,系统实际分配的内存大小

    下面的Mems.size内部其实也是调用malloc_size来获取实际占用内存大小

    下面类的实例对象占用多少内存?

    class Point {
        var x = 11
        var test = true
        var y = 22
    }
    var p = Point() 
    print(class_getInstanceSize(type(of: p))) // 40
    print( class_getInstanceSize(Point.self)) // 40
    print(Mems.size(ofRef: p)) // 48
    

    运行上面代码,打印40,说明p对象至少占用40字节内存(指向类型信息占用8,引用计数占用8,x占用8,y占用8,test占用1,实际一共占33字节,由于内存对齐是8,所以至少占用40),系统最终分配48字节(因为对象占用内存大小必须是16的倍数)

    • 在Mac、iOS中的malloc函数分配的内存大小总是16的倍数
     //传入一个指针,告诉你指针指向堆空间占用内存多大,如果传入不是堆空间的指针就返回0
    var ptr = malloc(17) //传入你想要多少字节
    print(malloc_size(ptr))
    

    当传入1打印16,传入16打印16,传入17打印32。

    • 类和结构体的比较
    func testClassAndStruct() {
        class Size {
            var width = 1
            var height = 2
        }
        
        struct Point {
            var x = 3
            var y = 4
        }
    
        print("------------------------")
    
        // 类
        var size = Size() 
    
        //至少占用32
        print(class_getInstanceSize(Size.self)) //32
        //系统最终分配32
        print(Mems.size(ofRef: size)) //32
         
        print("size变量的地址", Mems.ptr(ofVal: &size))
        print("size变量的内存", Mems.memStr(ofVal: &size))
         
        print("size所指向内存的地址", Mems.ptr(ofRef: size))
        print("size所指向内存的内容", Mems.memStr(ofRef: size))
         
        print("------------------------")
        
        // 结构体
        var point = Point()
    
        //至少占用16
        print("MemoryLayout<Point>.stride", MemoryLayout<Point>.size) //16
        //系统最终分配16
        print("MemoryLayout<Point>.stride", MemoryLayout<Point>.stride) //16
        //内存对齐8
        print("MemoryLayout<Point>.stride", MemoryLayout<Point>.alignment) //8
    
        print("point变量的地址", Mems.ptr(ofVal: &point))
        print("point变量的内存", Mems.memStr(ofVal: &point))
    }
    
    testClassAndStruct()
    

    打印:

    ------------------------
    32
    32
    size变量的地址 0x00007ffeefbff540
    size变量的内存 0x00000001018008b0
    size所指向内存的地址 0x00000001018008b0
    size所指向内存的内容 0x00000001000089d8 0x0000000200000002 0x0000000000000001 0x0000000000000002
    (分别对应:指向类型信息、引用计数、1、2)
    ------------------------
    16
    point变量的地址 0x00007ffeefbff510
    point变量的内存 0x0000000000000003 0x0000000000000004
    (分别对应:3、4)
    

    通过打印可以看出:

    1. size对象占用32字节,可以使用上面的两种方式打印出来
    2. size所指向内存的内容 0x00000001000089d8 0x0000000200000002 0x0000000000000001 0x0000000000000002,分别对应:指向类型信息、引用计数、1、2
    3. point结构体系统实际分配16字节
    4. point变量的内存 0x0000000000000003 0x0000000000000004,存放的分别是3、4的值

    注意:获取指针变量指向地址占用多少内存不能用如下方式:

    print("MemoryLayout<Size>.stride", MemoryLayout<Size>.stride)
    

    MemoryLayout是看某种类型变量占用多少内存,传入Size,由于Size是类,所以Size变量是指针类型,所以最后都当成指针,所以打印永远是8字节。

    注意:结构体存放在哪取决于你在哪里定义的。

    1. 如果结构体是在函数里面定义的那结构体就在栈空间
    2. 如果结构体是在函数外面定义的,那么它的内存就在数据段
    3. 如果类里面定义个结构体,那么这个结构体肯定跟随对象在堆空间。
    4. 但是类,无论你在哪里创建类,类的实例对象的内存一定在堆空间。但是类对应的指针变量的内存在哪里就不一定了,和上面类似。如果不明白可以参考:内存区域、Tagged Pointer

    五. 其他补充

    • 嵌套类型
    嵌套类型.png
    • 枚举、结构体、类都可以定义方法

    一般把定义在枚举、结构体、类内部的函数叫做方法,其他的都叫函数。

    枚举、结构体、类都可以定义方法.png

    注意:方法不占用对象的内存,无论方法定义在哪里(即使在类内部),因为方法的本质就是函数,函数都存放在代码段。

    • 思考:以下结构体、类对象的内存结构是怎样的?

    结构体:

    func testStruct() {
        struct Point {
            var x: Int
            var b1: Bool
            var b2: Bool
            var y: Int
        }
        var p = Point(x: 10, b1: true, b2: true, y: 20)
        
        print("MemoryLayout<Size>.size", MemoryLayout<Point>.size) //18
        print("MemoryLayout<Size>.stride", MemoryLayout<Point>.stride) //24
        print("MemoryLayout<Size>.alignment", MemoryLayout<Point>.alignment) //8
    }
    
    testStruct()
    

    可以发现,至少占用18字节,内存对齐8,系统实际分配24字节。两个Int占用8 + 8 = 16字节,两个Bool只需要2字节,一共18字节,对齐参数是8,所以占用24字节。

    类:

    func testClass() {
        class Size {
            var width: Int
            var b1: Bool
            var b2: Bool
            var height: Int
            init(width: Int, b1: Bool, b2: Bool, height: Int) {
                self.width = width
                self.b1 = b1
                self.b2 = b2
                self.height = height 
            }
        }
        var s = Size(width: 10, b1: true, b2: true, height: 20)
        
        print(class_getInstanceSize(type(of: s))) // 40
        print( class_getInstanceSize(Size.self)) // 40
        print(Mems.size(ofRef: s)) // 48
    }
    
    testClass()
    

    可以发现,至少占用40字节,实际分配48字节。类型信息占用8字节,引用计数占用8字节,两个Int占用8 + 8 = 16字节,两个Bool只需要2字节,一共34字节,由于内存对齐是8,所以至少占用40字节,又由于对象占用内存大小必须是16倍数,所以实际占用48字节。

    相关文章

      网友评论

          本文标题:iOS-Swift-结构体和类

          本文链接:https://www.haomeiwen.com/subject/fplkactx.html