Swift系列六 - 結構體與類的本質區別


在Swift標准庫中,絕大多數的公開類型都是結構體,而枚舉和類只占很小一部分。

一、結構體

常見的Bool、Int、Double、String、Array、Dictionary等常見類型都是結構體。

自定義結構體:

struct Date {
    var year: Int;
    var month: Int;
    var day: Int;
}
var date = Date(year: 2019, month: 06, day: 02)

所有結構體都有一個編譯器自動生成的初始化器(initializer、初始化方法、構造器、構造方法)。

Date(year: 2019, month: 06, day: 02)傳入的是所有成員值,用來初始化所有成員(叫做存儲屬性)。

1.1. 結構體的初始化器

編譯器會根據情況,可能會為結構體生成多個初始化器,宗旨是:保證所有成員都有初始值。

從上面案例可以看出,編譯器幫助生成初始化器的條件就是:讓所有存儲屬性都有值。

> 思考:下面的代碼能否編譯通過?

可選項都有個默認值nil,所以可以編譯通過。

1.2. 自定義初始化器

一旦在定義結構體時自定義了初始化器,編譯器就不會再幫它自動生成其他初始化器。

1.3. 探究結構體初始化器的本質

下面的兩段代碼是等效的:

代碼一:

代碼二:

經過對比發現,代碼一和代碼二的init方法完全一樣。也就是說,存儲屬性的初始化是在初始化構造方法中完成的。

1.4. 結構體的內存結構

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

/*
 輸出:
 0x000000000000000a 0x0000000000000014 0x0000000000000001
 17
 24
 8
 */

因為存儲屬性xy各占8個字節(連續內存地址),Bool在內存中占用1個字節,所以Point一共占用17個字節,由於內存對齊是8,所以一共分配了24個字節。

二、類

類的定義和結構體類似,但編譯器並沒有為類自動生成可以傳入成員值的初始化器。

定義類:

如果存儲屬性沒有初始值,無參的初始化器也不會自動生成:

如果把上面的類換成結構體(struct)類型就不會報錯:

2.1. 類的初始化器

如果類的所有成員都在定義的時候指定了初始值,編譯器會為類生成無參的初始化器。

成員的初始化是在這個初始化器中完成的。

下面的兩段代碼是等效的:
代碼一:

class Point {
    var x: Int = 0
    var y: Int = 0
}

var p1 = Point()

代碼二:

class Point {
    var x: Int
    var y: Int
    init() {
        self.x = 0
        self.y = 0
    }
}

var p1 = Point()

三、結構體與類的本質區別

結構體時值類型(枚舉也是值類型),類是引用類型(指針類型)。

3.1. 內存分析結構體與類

示例代碼:

class Size {
    var width: Int = 1
    var height: Int = 2
}

struct Point {
    var x: Int = 3
    var y: Int = 4
}

func test() {
    var size = Size()
    print("class-size對象的內存",Mems.memStr(ofRef: size))
    print("class-size指針的內存地址",Mems.ptr(ofVal: &amp;size))
    print("class-size對象的內存地址",Mems.ptr(ofRef: size))
    print("class-size.width的內存地址",Mems.ptr(ofVal: &amp;size.width))
    print("class-size.height的內存地址",Mems.ptr(ofVal: &amp;size.height))
    var point = Point()
    print("struct-point對象的內存",Mems.memStr(ofVal: &amp;point))
    print("struct-point的內存地址",Mems.ptr(ofVal: &amp;point))
    print("struct-point.x的內存地址",Mems.ptr(ofVal: &amp;point.x))
    print("struct-point.y的內存地址",Mems.ptr(ofVal: &amp;point.y))
}
test()
/*
 輸出:
 class-size對象的內存 0x00000001000092a8 0x0000000200000002 0x0000000000000001 0x0000000000000002
 class-size指針的內存地址 0x00007ffeefbff4d0
 class-size對象的內存地址 0x000000010061fe80
 class-size.width的內存地址 0x000000010061fe90
 class-size.height的內存地址 0x000000010061fe98
 
 struct-point對象的內存 0x0000000000000003 0x0000000000000004
 struct-point的內存地址 0x00007ffeefbff470
 struct-point.x的內存地址 0x00007ffeefbff470
 struct-point.y的內存地址 0x00007ffeefbff478
 */

示例代碼的在內存中:

經過分析可以看到,結構體的數據是直接存到棧空間的,類的實例是用指針指向堆空間的內存,指針在棧空間。上面示例代碼中類的實例占用32個字節,其中前面16個字節分別存儲指向類型信息和引用計數,后面16個字節才是真正用來存儲數據的。而結構體占用的內存大小等於存儲屬性所占內存大小之和。

> 注意:在C語言中,結構體是不能定義方法的,但是在C++Swift中,可以在結構體和類中定義方法。在64bit環境中,指針占用8個字節。
>> 擴展:值類型(結構體、枚舉)的內存根據所處的位置不同,內存的位置也不一樣。例如,定義一個全局的結構體,內存在數據段(全局區)中;如果在函數中定義,內存存放在棧空間;如果在類中定義一個結構體,內存跟隨對象在堆空間。

3.2. 匯編分析結構體與類

Swift中,創建類的實例對象,要向堆空間申請內存,大概流程如下:

  • Class.__allocating_init()
  • libswiftCore.dylib:_swift_allocObject_
  • libswiftCore.dylib:swift_slowAlloc
  • libsystem_malloc.dylib:malloc

在Mac,iOS中的malloc函數分配的內存大小總是16的倍數(為了做內存優化)。

通過class_getInstanceSize可以得知類的對象真正使用的內存大小。

import Foundation

class Point {
    var x: Int = 3
    var y: Int = 4
    var b: Bool = true
}
var p = Point()
print(class_getInstanceSize(type(of: p)))
print(class_getInstanceSize(Point.self))
/*
 輸出:
 40
 40
 */

內存占用大小 = 8(指向類型信息) + 8(引用計數) + 8(存儲屬性x) + 8(存儲屬性y) + 1(存儲屬性b) = 33;

內存分配大小 = 8(指向類型信息) + 8(引用計數) + 8(存儲屬性x) + 8(存儲屬性y) + Max(1(存儲屬性b), 8(內存對齊數)) = 40;

> 擴展:如果底層調用了alloc或malloc函數,說明該對象存在堆空間,否則就是在棧空間。

3.2.1. 匯編分析結構體

第一步:創建結構體,打斷點進入匯編:

第二步:在callq...init()函數處進入函數實現體(lldb進入函數體指令:si):

結論:rbp就是局部變量,所以結構體創建的對象是在棧中存儲的。

> 擴展:一般情況下,rbp就是局部變量,rip是全局變量,ret是函數返回。

3.2.2. 匯編分析類

第一步:創建結構體,打斷點進入匯編:

第二步:在callq...__allocating_init()...函數處打斷點,進入函數體:

第三步:在callq...swift_allocObject函數處打斷點,進入函數體:

第四步:一直進入到libswiftCore.dylib swift_allocObject:中,在callq...swift_slowAlloc處打斷點進入:

第五步:malloc出現了,這時候繼續進入函數體:

第六步:最終,對象是在libsystem_malloc.dylib庫中執行的malloc

經過上面分析,可以清晰的看到,對象是在堆空間存儲的。

擴展:在Mac、iOS中,創建對象都是調用的libsystem_malloc.dylib動態庫。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM