结构体和类

结构体

  • 在swift标准库中,绝大多数的公开类型都是结构体,枚举和类只占很小一部分
    • 比如Bool、Int、Double、String、Array、Dictionary等常见类型都是结构体
  • 所有的结构体都有一个编译器自动生成的初始化器(initializer、初始化方法、构造器、构造方法)
struct Date {
    var year: Int // 存储属性
    var month: Int
    var day: Int
}
// 这里调用的,就是初始化器,可以传入所有成员值,用以初始化所有成员(存储属性、Stored Property)
var date = Date(year: 2019, month: 8, day: 2)

结构体的初始化器

  • 编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是: 保证所有成员有初始值
struct Point {
    var x: Int
    var y: Int
}
var p1 = Point(x: 10, y: 20)
var p2 = Point(y: 32) // 错误 x没有初始化

struct Point {
    var x: Int = 0
    var y: Int
}
var p2 = Point(y: 32)// 正确 编译器提供了2种初始化器

struct Point {
    var x: Int?
    var y: Int?
}
var p1 = Point(x: 10, y: 20)
var p2 = Point(y: 32)
var p3 = 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: 20)
var p2 = Point(y: 32)// 不能调用init 初始化器
var p3 = Point() // 同上

初始化器的本质

  • 下面2个代码完全等效
    struct Point {
        var x: Int
        var y: Int
        init() {
            x = 0
            y = 0
        }
    }
    var p = Point()
    
    struct Point {
        var x: Int = 0
        var y: Int = 0
    }
    // 
    var p = Point()
    // 对应的汇编代码 相同
    TestS`init() in Point #1 in testStruct():
->  0x100001bf0 <+0>:  pushq  %rbp
    0x100001bf1 <+1>:  movq   %rsp, %rbp
    0x100001bf4 <+4>:  xorps  %xmm0, %xmm0
    0x100001bf7 <+7>:  movaps %xmm0, -0x10(%rbp)
    0x100001bfb <+11>: movq   $0x0, -0x10(%rbp)
    0x100001c03 <+19>: movq   $0x0, -0x8(%rbp)
    0x100001c0b <+27>: xorl   %eax, %eax
    0x100001c0d <+29>: movl   %eax, %ecx
    0x100001c0f <+31>: movq   %rcx, %rax
    0x100001c12 <+34>: movq   %rcx, %rdx
    0x100001c15 <+37>: popq   %rbp
    0x100001c16 <+38>: retq   
  • 结构体的内存布局
    struct Point {
        var x: Int = 10
        var y: Int = 14
        var origin: Bool = true
    }
    var p = Point()
    print(Mems.memStr(ofVal: &p)) // 0x000000000000000a 0x000000000000000e 0x0000000000000001
    print(MemoryLayout<Point>.size)// 17
    print(MemoryLayout<Point>.stride) // 24
    print(MemoryLayout<Point>.alignment) // 8

  • 类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器
  • 如果类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器
    • 成员的初始化实在这个初始化器中完成的
class Point {
        var x: Int = 10
        var y: Int = 14    
    }
    let p1 = Point()

结构体和类的本质区别

  • 结构体时值类型,类是引用类型(指针类型)
class Size {
        var width = 1
        var height = 2
    }
    
    struct Point {
        var x = 3
        var y = 4
    }
    
    var size = Size()
    // 类不论你在哪里创建,对象内存都是在堆空间,而指针变量则要看在哪里创建
    print("size所指向内存的大小",Mems.size(ofRef: size));
    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()
    // 结构体变量在函数里定义内存在栈空间,在函数外全局定义则是在数据段全局区,如果在类里则是在堆空间
    // 结构体的内存在哪取决于定义的位置
    print("point变量的地址",Mems.ptr(ofVal: &point))
    print("point变量的内存",Mems.memStr(ofVal: &point))
    /*
     size所指向内存的大小 32
     size变量的地址 0x00007ffeefbff580
     size变量的内存 0x0000000100712bc0
     size所指向内存的地址 0x0000000100712bc0
     size所指向内存的内容  0x0000000100007778 对象的类型信息
                        0x0000000200000002 引用计数相关
                        0x0000000000000001 0x0000000000000002 // 成员变量
                        这个占用32个字节
     ------------------------------
     point变量的地址 0x00007ffeefbff560
     point变量的内存 0x0000000000000003 0x0000000000000004
    */
  • 对象在堆空间内存申请过程
    • Class._allocating_init()
    • libswiftCore.dylib:_swift_allocObject
    • libswiftCore.dylib:_swift_slowAlloc
    • libsystem_malloc.dylib:malloc
    • 在mac、ios中malloc函数分配的内存大小总是16的倍数

值类型

  • 值类型赋值给var、let或者给函数传参,是直接将所有内容拷贝一份

    • 类似于对文件进行copy、paste操作一样,产生了全新的文件副本,属于深拷贝
  • 汇编分析

// 结构体在全局区 在数据段
struct Point {
    var x: Int
    var y: Int
}
var p1 = Point(x: 10, y: 20)
movq   %rax, 0x790(%rip) // 0x100001978 + 0x790 = 0x100002108
movq   %rdx, 0x791(%rip) // 0x10000197f + 0x791 = 0x100002110 
var p2 = p1
movq   0x763(%rip), %rax // 0x1000019a5 + 0x763 = 0x100002108 --> 10
movq   %rax, 0x76c(%rip) // 10放到 0x1000019b3 + 0x76c = 0x10000211F -->p2的内存地址
movq   0x75d(%rip), %rax // 20
movq   %rax, 0x766(%rip) // 20 放到 0x1000019be + 0x766 = 0x100002124

p2.x = 11
p2.y = 22

// 结构体在函数里的情况
func testValueType() {

    struct Point {
        var x: Int
        var y: Int
    }
    var p1 = Point(x: 10, y: 20)
    var p2 = p1
    // rax 里是10  rdx 是20
    movq   %rax, -0x10(%rbp) // rbp -0x10 p1 的内存地址
    movq   %rdx, -0x8(%rbp) // rbp -0x8  连续的16个字节

    movq   %rax, -0x20(%rbp) // rbp -0x20 p2 的内存地址
    movq   %rdx, -0x18(%rbp) // // rbp -0x18 连续的16个字节

    p2.x = 11
    p2.y = 22

    movq   $0xb, -0x20(%rbp)
    movq   $0x16, -0x18(%rbp) // 11 和 22 给了上面p2的内存

    print("123")
}
testValueType()

  • 规律

    • 内存地址格式为: 0x3bdc(%rip),一般是全局变量,全局区(数据段)
    • 内存地址格式为: -0x78(%rbp),一般是局部变量,栈空间
    • 内存地址格式为: -0x10(%rax),一般是堆空间
  • 在Swift标准库中,为了提升性能,String、Array、Dictoinary、Set采用了 Copy On Write技术

    • 比如仅当有"写"的操作时,才会真正执行深拷贝操作
    • 建议: 不需要修改的尽量定义为let
    • 如果是自定义值类型,是不会采用自动采用Copy On Write技术

引用类型

  • 值类型赋值给var、let或者给函数传参,是将内存地址拷贝一份
    • 类似于制作一个文件的替身(快捷方式、链接),指向的是同一个文件,属于浅拷贝(shallow copy)
func testReferenceType() {
    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)
    // rax 是返回值的地址 -0x10(%rbp)局部变量地址 就是s1的地址 rax = 0x0000000100706ea0
    // rax 里的r内容 在堆里
    // A0 21 00 00 01 00 00 00 指向类型信息
    // 02 00 00 00 00 00 00 00 引用计数
    // 0A 00 00 00 00 00 00 00 --> 10
    // 14 00 00 00 00 00 00 00 --> 20
    movq   %rax, -0x10(%rbp)
    
    var s2 = s1
    // -0x60(%rbp) s2的地址
    movq   %rax, -0x60(%rbp)
    
    s2.width = 11
    // 将s2的地址给了rax 然后rax+10  刚好是s2存储10的位置
    movq   -0x60(%rbp), %rax
    // 将11 放到了 rax+0x10 刚好是s2存储10的位置
    movq   $0xb, 0x10(%rax)
    
    s2.height = 22
    // 将$0x16 22  放到了s2第二个成员的位置
    $0x16, 0x18(%rax)
}
testReferenceType()

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

  • 一般把定义在枚举、结构体、类内部的函数,叫做方法
    • 要使用实例调用方法
    • 方法不占用对象的内存
    • 方法的本质就是函数,函数、方法都存放在代码段
posted @ 2021-04-14 16:08  YALMiOS  阅读(95)  评论(0)    收藏  举报