值類型
值類型
是一種當它被指定到常量或者變量,或者被傳遞給函數(shù)時會被拷貝的類型。
Swift 中所有的基本類型——整數(shù)
,浮點數(shù)
,布爾量
,字符串
,數(shù)組
和字典
——都是值類型,并且都以結(jié)構(gòu)體的形式
在后臺實現(xiàn)。
Swift 中所有的結(jié)構(gòu)體
和枚舉
都是值類型
內(nèi)存的五大分區(qū)
- Stack (局部變量 & 調(diào)用上下文),系統(tǒng)管理的,是連續(xù)的內(nèi)存空間
- Heap (new & malloc),程序員管理,類似于鏈表
- 全局區(qū)
- 字符區(qū)(字符串 & 常量)
- __text(指令,即代碼段)
內(nèi)存地址
- 棧區(qū)的地址 比 堆區(qū)的地址 大
- 棧是從
高地址->低地址
,向下延伸,由系統(tǒng)
自動管理,是一片連續(xù)的內(nèi)存空間 - 堆是從
低地址->高地址
,向上延伸,由程序員
管理,堆空間結(jié)構(gòu)類似于鏈表
,是不連續(xù)的 - 日常開發(fā)中的溢出是指
堆棧溢出
,可以理解為棧區(qū)與堆區(qū)邊界碰撞的情況 -
全局區(qū)、常量區(qū)
都存儲在Mach-O
中的__TEXT cString
段
什么是值類型
通過下列代碼來分析值類型
func test(){
//棧區(qū)聲明一個地址,用來存儲age變量
var age = 18
//傳遞的值
var age2 = age
//age、age2是修改獨立內(nèi)存中的值
age = 30
age2 = 45
print("age=\(age),age2=\(age2)")
}
test()
從例子中可以得出,age存儲在棧區(qū)
- 查看
age
的內(nèi)存情況,從圖中可以看出,棧區(qū)直接存儲
的是值
- 獲取
age
的棧區(qū)地址:po withUnsafePointer(to: &age) { print($0) }
- 查看
age
的內(nèi)存情況:x/8g 0x00007ffeefbff480
- 獲取
- 查看
age2
的情況,從下圖中可以看出,age2
的賦值相當于將age
中的值拿出來,賦值給了age2
。其中age
與age2
的地址 相差了8
字節(jié),從這里可以說明棧空間是連續(xù)的
、且是從高到低的
從上面可以得出,age就是值類型
值類型的特點
- 地址中存儲的是
值
- 值類型的傳遞過程中,相當于傳遞了一個
副本
,也就是所謂的深拷貝
- 值傳遞過程中,并不共享狀態(tài)
結(jié)構(gòu)體
結(jié)構(gòu)體的初始化
定義一個結(jié)構(gòu)體
// ***** 未定義init方法 *****
struct HTTeacher {
var age: Int = 18
func teacher() {
print("teacher")
}
}
// ***** 自定義init方法 *****
struct HTTeacher {
var age: Int = 18
func teacher() {
print("teacher")
}
init(age: Int) {
self.age = age
}
}
var t = HTTeacher(age: 20)
- 在結(jié)構(gòu)體中,如果不給屬性默認值,編譯是不會報錯的。即在結(jié)構(gòu)體的定義中,屬性可以賦值,也可以不賦值
-
init
方法可以重寫,也可以使用系統(tǒng)默認的
結(jié)構(gòu)體的SIL分析
- 如果沒有自定義
init方法
,系統(tǒng)會提供默認的初始化方法
默認初始化方法 - 如果提供了
自定義的init方法
,就只有自定義的方法
自定義初始化方法
結(jié)構(gòu)體是值類型
定義一個結(jié)構(gòu)體,并進行分析
struct HTTeacher {
var age: Int = 18
var age2: Int = 20
}
var t = HTTeacher()
print("end")
- 通過
po t
發(fā)現(xiàn),打印結(jié)果就是值,沒有任何與地址有關(guān)的信息
-
獲取t的內(nèi)存地址,并查看其內(nèi)存情況
- 獲取t的地址:
po withUnsafePointer(to: &t) { print($0) }
- 查看內(nèi)存情況:
x/8g 0x0000000100008178
結(jié)構(gòu)體 - 獲取t的地址:
問題:此時將t賦值給t1,如果修改了t1,t會發(fā)生改變嗎?
當 t2
被賦予 t
的當前值,存儲在 t
中的值就被拷貝給了新的 t2
實例。這最終的結(jié)果是兩個完全不同的實例
,它們只是碰巧包含了相同的數(shù)字值。由于它們是完全不同的實例, t2
的age
被設(shè)置 30并不影響 t
中 age
存儲的值。
SIL分析
生成SIL文件
:swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil
在SIL
文件中,我們查看結(jié)構(gòu)體的初始化方法,可以發(fā)現(xiàn)只有init
,而沒有malloc
,在其中看不到任何關(guān)于堆區(qū)的分配
總結(jié)
-
結(jié)構(gòu)體是值類型
,且結(jié)構(gòu)體的地址就是第一個成員的內(nèi)存地址 - 值類型
- 在內(nèi)存中直接
存儲值
- 值類型的賦值,是一個
值傳遞
的過程,即相當于拷貝了一個副本,存入不同的內(nèi)存空間,兩個空間彼此間并不共享狀態(tài)
-
值傳遞
其實就是深拷貝
- 在內(nèi)存中直接
mutating 方法異變
結(jié)構(gòu)體
和枚舉
是值類型
。默認情況下,值類型屬性不能被自身的實例方法
修改。
- 修改
push
方法如下,查看SIL文件
:swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil
從上圖中可以看出,
push函數(shù)
除了item
,還有一個默認參數(shù)self
,這兩個參數(shù)都是let
類型,是不允許修改的
- 如果你需要在特定的方法中
修改
結(jié)構(gòu)體或者枚舉的屬性,你可以選擇將這個方法異變
。 - 你可以選擇在
func
關(guān)鍵字前放一個mutating
關(guān)鍵字來使用這個行為
struct HTStack {
var items: [Int] = []
mutating func push(_ item: Int) {
items.append(item)
}
}
查看SIL文件
,找到 push
函數(shù),發(fā)現(xiàn)與之前有所不同。添加 mutating
后,本質(zhì)是給值類型函數(shù)
添加了 inout
關(guān)鍵字,相當于在值傳遞過程中,傳遞的是引用
(即地址)
inout關(guān)鍵字
一般情況下,在函數(shù)、方法的聲明中,參數(shù)
都是默認不可變的
,如果想要直接修改,需要給參數(shù)加上 inout
關(guān)鍵字。
- 未加
inout
關(guān)鍵字,給參數(shù)賦值,編譯報錯
- 添加
inout
關(guān)鍵字,可以給參數(shù)賦值,調(diào)用的時候需要加上&
總結(jié)
- 結(jié)構(gòu)體中的函數(shù)如果想修改其中的屬性,需要在函數(shù)前加上
mutating
,而類則不用 -
mutating
的本質(zhì)是給self
加了inout
關(guān)鍵字 -
inout
相當于取地址
,可以理解為地址傳遞
,即引用 -
mutating
修飾方法
,inout
修飾參數(shù)
引用類型-類
類的定義
struct HTTeahcer {
var age: Int = 18
var age2: Int = 20
}
class HTTeacher1 {
var age: Int = 18
var age2: Int = 20
}
//結(jié)構(gòu)體:值類型
var t = HTTeahcer()
// 類: 引用類型
// t1 存儲在全局區(qū)
var t1 = HTTeacher1()
打印t、t1, 從下圖中可以發(fā)現(xiàn),t中存儲的是值,t1
內(nèi)存空間中存儲的是地址
獲取t1變量的地址,并查看內(nèi)存情況
- 獲取
t1
的指針地址:po withUnsafePointer(to: &t1) { print($0) }
- 查看
t1
全局區(qū)內(nèi)存地址內(nèi)存情況:x/8g 0x0000000100008368
- 查看
t1
地址中存儲的堆區(qū)地址內(nèi)存情況:x/8g 0x00000001004425d0
引用類型的特點
- 1、地址中存儲的是
堆區(qū)地址
- 2、堆區(qū)地址中存儲的是
值
問題1:此時將t1賦值給t2,如果修改了t2,會導(dǎo)致t1修改嗎?
- 通過
lldb
調(diào)試得知,修改t2
的值,會導(dǎo)致t1改變
。主要是因為t1
、t2
地址中存儲的是同一個堆區(qū)地址
,如果修改,修改的是同一個堆區(qū)地址,所以修改t2會導(dǎo)致t1一起修改,即淺拷貝
問題2:如果結(jié)構(gòu)體中包含類對象,此時如果修改t1中的實例對象屬性,t會改變嗎?
代碼如下:
struct HTTeahcer {
var age: Int = 18
var age2: Int = 20
var teacher: HTTeacher1 = HTTeacher1()
}
class HTTeacher1 {
var age: Int = 18
var age2: Int = 20
}
var t = HTTeahcer()
var t1 = t
t1.teacher.age = 30
print(t.teacher.age) // 30
print(t1.teacher.age) // 30
雖然在結(jié)構(gòu)體中是值傳遞
,但是對于teacher
,由于是引用類型
,所以傳遞的依然是地址
注意:在編寫代碼過程中,應(yīng)該盡量避免值類型包含引用類型
SIL分析
查看當前的SIL文件
,盡管HTTeacher1
是放在值類型中的,在傳遞的過程中,不管是傳遞還是賦值,teacher
都是按照引用計數(shù)
開始管理的
通過po CFGetRetainCount(t.teacher)
查看,teacher
的引用計數(shù)為3
主要是因為:
-
main
中retain一次 -
teacher.getter
方法中retain一次 -
teacher.setter
方法中retain一次
總結(jié)
通過上述 LLDB
查看結(jié)構(gòu)體和類的內(nèi)存模型,有以下結(jié)論:
-
值類型
,相當于一個本地Excel
,當我們給你一個 excel文件時,就相當于一個值類型傳遞,你修改了什么我們時不知道的 -
引用類型
,相當于一個在線表格
,當我們和你共同編輯一個在線表格時,就相當于一個引用類型傳遞,兩邊都會看到修改的內(nèi)容 -
結(jié)構(gòu)體
中方法修改屬性
,需要在方法前添加mutating
關(guān)鍵字,本質(zhì)是給函數(shù)的默認參數(shù)self
添加inout
關(guān)鍵字,將self
從let
變量變成var
變量