1平酿、結(jié)構(gòu)體
結(jié)構(gòu)體都有一個編譯器自動生成的初始化器塞椎。
根據(jù)情況可能會生成多個初始化器,保證所有的成員(存儲屬性喜庞、Stored Property)都有初始值。
struct Person {
var name: String
var age: Int
}
//正確
var person = Person(name: "wang", age: 18)
//錯誤
var person1 = Person(name: "li")
var person2 = Person(age: 20)
var person3 = Person()
可以看到我們在確保所有的儲存屬性有值的情況下去初始化才是正確的棋返。
那我們也可以這樣去定義屬性
struct Person {
var name: String = ""
var age: Int = 0
}
var person = Person(name: "wang", age: 18)
var person1 = Person(name: "li")
var person2 = Person(age: 20)
var person3 = Person()
這種情況下下面的即中初始化方法均可男摧,因為我們已經(jīng)給我們的存儲屬性賦了初值了。
那同樣吹零,我們也可以給定其初始值為nil
柠新,這樣也可以使用下面的初始化方法。
當然射沟,我們也可以自定義初始化器殊者。
struct Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
同樣我們也可以在自定義的初始化器中給定其默認值。
struct Person {
var age: Int?
init() {
self.age = 10
}
}
var person = Person()
此時我們通過匯編進入看一下验夯,可以看到其調(diào)用了init
函數(shù)
通過命令
si
進入該函數(shù)可以看到在第8行處將10賦值到一處內(nèi)存中猖吴。
但如果我們沒有自定義初始化器呢?也就是直接給定其一個默認值的情況下挥转,其內(nèi)部是怎么調(diào)用的呢海蔽?
struct Person {
var age: Int = 10
}
var person = Person()
通過斷點走入?yún)R編代碼共屈。可以看到與上面相比基本是一模一樣的党窜,甚至連調(diào)用方法的地址都是一致的拗引。
可以看到仍然是調(diào)用了
init
方法,同樣進入該方法中,可以看到也是有賦值操作的刑然。也就是說寺擂,不管我們是通過自定義初始化器給定屬性默認值還是直接定義屬性時給定其默認值都是在
init
方法中將數(shù)據(jù)寫入屬性中去的。
至于結(jié)構(gòu)體在內(nèi)存中的布局其實與上一期我們講到的枚舉是一致的泼掠。
比如上面我們實例化的person
對象怔软,其有一個屬性為age
,該屬性在內(nèi)存中占8
個字節(jié)择镇,又因為其內(nèi)存對齊字節(jié)為8
挡逼,因此其內(nèi)存中的布局為0x000000000000000a
。
2腻豌、類
相比結(jié)構(gòu)體而言類是沒有可以傳入成員值的初始化器的家坎。
同樣,當我們定義了類以后沒有給其成員默認值的情況下也是會有錯誤提示的
但是類因為沒有默認的成員初始化器吝梅,因此我們直接在初始化時傳入成員屬性時不可行的:
此時需要我們手動去給其實現(xiàn)初始化器虱疏。
3、區(qū)別
其實結(jié)構(gòu)體和類的本質(zhì)區(qū)別在于結(jié)構(gòu)體是值類型而類是引用類型苏携。
也就是說結(jié)構(gòu)體對象其位于內(nèi)存中即椬龅桑空間中,而引用類型其真正的內(nèi)存空間在堆空間中右冻。
但嚴格來說我們并不能說結(jié)構(gòu)體內(nèi)存一定是在椬芭睿空間,其內(nèi)存分配在哪兒取決于它定義的位置纱扭,如果其在函數(shù)內(nèi)定義的那么其內(nèi)存在楇怪悖空間內(nèi)。如果其在外部定義乳蛾,那么其內(nèi)存在數(shù)據(jù)段內(nèi)(全局區(qū))暗赶。如果其作為一個類的屬性,那么其內(nèi)存自然是會分配在堆空間屡久。
至于類的話不管在哪兒定義其內(nèi)存一定是在堆空間忆首,在不同的地方定義改變的只是其指針的內(nèi)存位置。
問題:那如果結(jié)構(gòu)體定義在一個類的方法里邊其內(nèi)存會在哪兒被环?
3.1 內(nèi)存分配
那么我們怎么去區(qū)分一個對象是在棧空間還是在堆空間详幽?
其實如果對象是在堆空間時其初始化時會牽涉到alloc
或者malloc
筛欢。
此時我們定義一個類對象并實例化浸锨,此時加上斷點
匯編開啟后會看到
此時可以看到在第14行處明顯的有調(diào)用
allocating_init
,即向堆空間申請內(nèi)存版姑,此時通過命令si
進入可以看到在第16行處有
allocObject
,此時進入柱搜,然后一直通過si
命令進入。最終到這個地方:
可以看到在這里調(diào)用了一個
slowAlloc
方法剥险。進入后可以看到在這里會調(diào)用系統(tǒng)級別的
malloc
方法分配內(nèi)存而同樣的我們可以看到結(jié)構(gòu)體會調(diào)用init
聪蘸,也就是直接分配在棧空間的表制。
//類對象堆空間申請內(nèi)存過程
接下來我們看看struct
和class
在內(nèi)存中的地址健爬。定義一個結(jié)構(gòu)體以及類,并實例化
struct StructObject {
var age: Int = 10
var count: Int = 11
}
class ClassObject {
var age: Int = 10
var count: Int = 20
}
var strutObject = StructObject()
var classObject = ClassObject()
print("結(jié)構(gòu)體變量的地址:",Mems.ptr(ofVal: &strutObject))
print("結(jié)構(gòu)體變量的內(nèi)容:",Mems.memStr(ofVal: &strutObject))
print("----------------")
print("類變量指針的地址:",Mems.ptr(ofVal: &classObject))
print("類變量指針的內(nèi)容:",Mems.memStr(ofVal: &classObject))
此時我們查看打印結(jié)果:
結(jié)構(gòu)體變量的地址: 0x00000001000083c8
結(jié)構(gòu)體變量的內(nèi)容: 0x000000000000000a 0x000000000000000b
----------------
類變量指針的地址: 0x00000001000083d8
類變量指針的內(nèi)容: 0x000000010079c950
其中0x00000001000083c8
和0x00000001000083d8
分別是兩個對象的地址么介。我們查看內(nèi)存中的值娜遵,可以看到前8個字節(jié)是Person.age
的值,后面8個字節(jié)是Person.count
的值
而類對應(yīng)的內(nèi)存中只是存放了另一個地址壤短,而這個地址實際上指向的就是堆空間设拟。
可是我們根據(jù)該地址去查看內(nèi)存中的數(shù)據(jù)時發(fā)現(xiàn)前8個字節(jié)以及第2個8個字節(jié)中放的數(shù)據(jù)不知道是啥,而從第3個把個字節(jié)才看到是對應(yīng)的屬性的值久脯。
其實類對應(yīng)的存儲空間中纳胧,前8個字節(jié)指向的是內(nèi)存信息,第2個8字節(jié)是引用計數(shù)信息帘撰,后面就是成員信息了跑慕。
我們通過計算類變量指針地址與結(jié)構(gòu)體變量的指針地址可以獲知其差了
0x10
,也就是16個字節(jié),那么0x00000001000083c8
這個地址存放0x000000000000000a
骡和,接下來8個字節(jié)存放0x000000000000000b
相赁,接下來的地址0x00000001000083d8
存放的就是0x000000010079c950
內(nèi)容,也就是這三個挨著的地址前兩個放著結(jié)構(gòu)體的內(nèi)容慰于,后一個存放著類對象的地址钮科。
我們也可以打印出類對象對應(yīng)的信息:
print("類變量指針所指向的內(nèi)容的地址:",Mems.ptr(ofRef: classObject))
print("類變量指針所指向的內(nèi)容的內(nèi)容:",Mems.memStr(ofRef: classObject))
類變量指針所指向的內(nèi)容的地址: 0x00000001006086f0
類變量指針所指向的內(nèi)容的內(nèi)容: 0x00000001000082c8 0x0000000200000002 0x000000000000000a 0x0000000000000014
可以看到指針中存放的就是類對象的地址,類對象的4個8字節(jié)即是前面所說的一些信息婆赠。
我們也可以分析結(jié)構(gòu)體對象和類對象在內(nèi)存中的大小绵脯。
上述我們定義的結(jié)構(gòu)體對象有2個屬性,那么其在內(nèi)存中的大小為16字節(jié)
休里。
類對象同樣也有兩個屬性蛆挫,但是指針僅占8字節(jié)
,而這8個字節(jié)中存的是類對象的地址妙黍,而真正的類對象是占用32個字節(jié)的悴侵。
上述結(jié)論可通過MemoryLayout
打印結(jié)果可知。
print("結(jié)構(gòu)體內(nèi)存大小",MemoryLayout<StructObject>.stride)
print("類對象指針內(nèi)存大小",MemoryLayout<ClassObject>.stride)
結(jié)構(gòu)體內(nèi)存大小 16
類對象指針內(nèi)存大小 8
3.2 值類型匯編分析
值類型為深拷貝(deep copy) 拭嫁,引用類型為淺拷貝(shallow copy)
深拷貝也就是賦值一份副本可免,即重新分配一塊內(nèi)存而淺拷貝僅僅是定義一個指針指向相同的內(nèi)存抓于。
func test() {
struct Auction {
var id: Int
var type: Int
}
let auction = Auction(id: 10, type: 20)
var p = auction
p.id = 11
p.type = 22
print("qeqwwq")
}
test()
同樣斷點分析匯編
直接找立即數(shù),可以看到在第8行和第9行處將10和20分別放入
edi
和esi
中,而實際上edi
和esi
內(nèi)容在rdi
和rsi
中可取得浇借,地市我們進入init
方法中:可以看到分別從
rdi
和rsi
中取值放入rax
和rdx
中捉撮,這個時候rax
和rdx
分別存放的就是10
和20
,此時再看第一張圖片中call
指令下面可以看到分別將rax
和rdx
中的值放入了-0x10(%rbp)
和-0x8(%rbp)
中,同時也放入了-0x20(%rbp)
和-0x18(%rbp)
中妇垢,其實這也就對應(yīng)了代碼的實例化auction
以及p = auction
巾遭,第15行和16行又將11
和22
放入到p
對應(yīng)的內(nèi)存中,也就是重新給p
賦值操作闯估。此時我們是將結(jié)構(gòu)體定義在函數(shù)內(nèi)灼舍,實際上我們將結(jié)構(gòu)體定義在外部也是一樣,只是稍顯復(fù)雜而已睬愤,但是其邏輯完全相同片仿。
在swift標準庫中字符串、數(shù)組尤辱、字典其底層都是結(jié)構(gòu)體砂豌,因此其也都是值類型。
而swift中為了提升性能它們采用 Copy On Write機制光督。也就是修改的時候才去copy阳距,如果不進行修改的時候仍然是同一份內(nèi)存。
當對同一個結(jié)構(gòu)體對象進行第二次實例化的時候其只是對原地址中的數(shù)據(jù)進行重新賦值结借,其地址不會改變筐摘。
var auction = Auction(id: 10, type: 20)
auction = Auction(id: 11, type: 22)
查看匯編,直接找到立即數(shù)
10
和20
可以看到分別賦值給edi
和esi
船老,也就是rdi
和rsi
咖熟。call
調(diào)用可函數(shù),進入可以看到柳畔,從rdi
和rsi
中取出內(nèi)容又給了rax
和rdx
馍管。
SwiftStudy`init(id:type:) in Auction #1 in test():
-> 0x1000010e0 <+0>: pushq %rbp
0x1000010e1 <+1>: movq %rsp, %rbp
0x1000010e4 <+4>: movq %rdi, %rax
0x1000010e7 <+7>: movq %rsi, %rdx
0x1000010ea <+10>: popq %rbp
0x1000010eb <+11>: retq
此時我們看匯編斷點的截圖,函數(shù)調(diào)用完成后又把rax
和rdx
中的值給了-0x10(%rbp)
和-0x8(%rbp)
薪韩,此時我們查看對應(yīng)的內(nèi)存空間:
可以看到對應(yīng)的正是
10
和20
确沸,那么對應(yīng)的地址0x7FFEEFBFF530
則是通過拿到rbp
的地址后計算得到對應(yīng)的地址。可以看到后面又重新走了賦值俘陷,調(diào)用
call
方法然后再賦值的操作罗捎。可以看到后面的賦值和前面的賦值的地址是一致的拉盾。
3.3 引用類型匯編分析
定義一個類
func testClass() {
class Person {
var age: Int
var height: Int
init(age: Int, height: Int) {
self.age = age
self.height = height
}
}
var person = Person(age: 10, height: 20)
var person1 = person
person1.age = 11
person1.height = 22
}
同樣桨菜,進入?yún)R編斷點后直接找對應(yīng)的立即數(shù),可以看待第13捉偏、14行對應(yīng)的分別是10
和20
雷激,分別放入寄存器edi
和esi
替蔬,然后執(zhí)行call
指令告私。
接下來調(diào)用了allocating_init
屎暇,進入該函數(shù)可以看到從對應(yīng)的寄存器中取值后給到了相鄰的兩個地址中。
結(jié)束函數(shù)調(diào)用驻粟,回到執(zhí)行call
處
可以知道根悼,函數(shù)調(diào)用結(jié)束后將rax
中的值給了-0x10(%rbp)
和-0x60(%rbp)
,
而根據(jù)我們的代碼可知蜀撑,在實例化person
后會將person
賦值給person1
挤巡。那么和這里的匯編代碼吻合了,也就是rax
存放的就是實例化Person
的地址酷麦,該地址分別給放在了對應(yīng)的兩個地址中矿卑。
-
rax
一般存放函數(shù)調(diào)用的返回值 -
-0x60(%rbp)
類似于這種rbp-地址值
的類型一般都是局部變量
獲取rax
存放的地址值,直接去查看改地址對應(yīng)的內(nèi)存
(lldb) register read rax
rax = 0x00000001005388f0
(lldb) memory read 0x00000001005388f0
0x1005388f0: c0 72 00 00 01 00 00 00 02 00 00 00 00 00 00 00 .r..............
0x100538900: 0a 00 00 00 00 00 00 00 14 00 00 00 00 00 00 00 ................
(lldb)
可以看到沃饶,存放數(shù)據(jù)的共有32
個字節(jié)母廷,第一個8字節(jié)
存放的是類信息,第2個8字節(jié)
存放的是引用計數(shù)信息糊肤,之后便是其兩個屬性數(shù)據(jù)了琴昆。
接下來我們?nèi)タ葱薷?code>person1的屬性值。同樣我們?nèi)フ覍?yīng)的立即數(shù)
可知馆揉,將兩個立即數(shù)放入到了對應(yīng)的兩個對應(yīng)的空間业舍。此時斷點在37行,獲取
rax
的地址升酣,此時的地址既是對應(yīng)的person
和person1
內(nèi)存放的地址舷暮。
(lldb) register read rax
rax = 0x00000001005388f0
而又分別在0x00000001005388f0
的基礎(chǔ)上移動了16
個字節(jié)(0x100538900
)和24
個字節(jié)(0x100538908
)對應(yīng)的不就是兩個屬性數(shù)據(jù)的地址,也就是這里直接是修改了屬性的值了噩茄,從而person
和person1
指向的堆空間的數(shù)據(jù)也改變了下面。
- 類似
rax+地址值
一般都是堆空間的地址