iOS-Swift-结构体和类

一. 结构体

1. 结构体简介

在 Swift 标准库中,绝大多数的公开类型都是结构体,而枚举和类只占很小一部分,如下:

常见数据类型
结构体.png

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

枚举:枚举可以使⽤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

以上两段代码完全等效,上面结构体已经验证过了,这里就不验证了,因为是类似的。
这也验证了我们上面所说的:结构体/类设置初始值,就相当于定义了一个无参初始化器

3. 自定义初始化器

编译器没有为类⾃动⽣成可以传⼊成员值的初始化器(想让我们⾃⼰写),但是如果属性都有默认值,也会帮我们创建⼀个⽆参初始化器。

三. 结构体与类的区别

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

意思就是s1指针,以前指向一个对象,重新赋值之后重新指向了另外一个对象。

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. 但是类,无论你在哪创建类,类的实例对象的内存一定在堆空间
  5. 但是类的实例对象对应的指针变量的内存在哪就不一定了,和上面类似。如果不明白可以参考:内存区域、Tagged Pointer

五. 其他补充

1. 结构体嵌套枚举

结构体嵌套枚举.png

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

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

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

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

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

结构体

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字节。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容