彙編深刻分析inout本質

本文首發於我的博客html

前言

關於輸入輸出參數inoutSwift之函數一文中,咱們已經有了初步的認識。如今咱們再繼續深刻了解一下git

輸入輸出參數

  • 說了形參只能是let,可是若是咱們想再內部修改外部實參的值,能夠用 inout 定義輸入輸出參數

例如github

func swapValues(_ v1: inout Int, _ v2: inout Int) {
        let tmp = v1
        v1 = v2
        v2 = tmp
        //前面三行也能夠換成  (v1, v2) = (v2, v1) 效果同樣
    }
    
     var num1 = 10
     var num2 = 20
     swapValues(&num1, &num2)
    print("num1 = \(num1), num2 = \(num2)")
       
     輸出: num1 = 20, num2 = 10 
        
複製代碼

注意點:編程

  • 可變參數不能標記爲inout
  • inout參數不能有默認值
  • inout參數的本質是地址傳遞(引用傳遞)
  • inout參數只能傳入能夠被屢次賦值的

準備代碼

inout 是地址傳遞,對於不一樣的狀況具體怎麼傳遞呢?彙編撥開雲霧swift

以下代碼,表示等邊的多邊形,其中width表示邊長,side表示多邊形邊長數量 girth表示周長,咱們知道 周長 = 邊長 * 邊數bash

struct Shape {
    // 寬、邊長
    var width: Int
    // 邊的數量
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetSide", oldValue, side)
        }
    }
    // 周長
    var girth: Int {
        set {
            // 邊長 = 周長 / 邊數
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            // 周長 = 邊長 * 邊數
            return width * side
        }
    }
    func show() {
        print("width=\(width), side=\(side), girth=\(girth)")
        
    }
}


func test(_ num: inout Int) {
 	print("test");
    num = 8
}
複製代碼

inout 修改存儲屬性

先看打印結果

以下代碼app

struct Shape {
    // 寬、邊長
    var width: Int
    // 邊的數量
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetSide", oldValue, side)
        }
    }
    // 周長
    var girth: Int {
        set {
            // 邊長 = 周長 / 邊數
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            // 周長 = 邊長 * 邊數
            return width * side
        }
    }
    func show() {
        print("width=\(width), side=\(side), girth=\(girth)")
        
    }
}


func test(_ num: inout Int) {
 	print("test");
    num = 8
}

var s = Shape(width: 10, side: 4)
test(&s.width) //這裏打斷點
s.show()
複製代碼

打印結果爲ide

test
getGirth
width=8, side=4, girth=32
複製代碼

其中getGirth這句打印是由於後面的show方法須要獲取girth的值,若是咱們去掉最後一句,只有以下代碼函數

var s = Shape(width: 10, side: 4)
test(&s.width)
複製代碼

輸出爲post

test
複製代碼

彙編分析

在上面代碼中的 test(&s.width)這一句打斷點

關鍵代碼爲

//全局變量0x44be(%rip)的地址給寄存器rdi,rdi是全局變量s的地址
 0x100000fbb <+107>: leaq   0x44be(%rip), %rdi        ; testSwift.s : testSwift.Shape

// 調用test函數,其中rdi做爲參數傳入
0x100000fc2 <+114>: callq  0x100001930               ; testSwift.test(inout Swift.Int) -> () at main.swift:44
複製代碼

小結

把屬性s.width的地址值傳遞過去,進行修改

  • 全局變量0x44be(%rip)的地址給寄存器rdi,rdi是全局變量s的地址

  • 調用test函數,其中rdi做爲參數傳入

  • 爲何咱們代碼中寫的是 s.width ,但彙編傳入的是s的地址呢?

    • 由於,width做爲結構體的第一個屬性變量,它的地址就是結構體s的地址
  • 爲何rdi是做爲參數呢?

    • 彙編總結中咱們知道 rdi、rsi、rdx、rcx、r八、r9等寄存器經常使用於存放函數參數。

inout 修改帶有屬性觀察器的存儲屬性

分析

首先分析一下,應該和前面存儲屬性不同的,由於若是直接修改存儲屬性side的值,那怎麼調動屬性觀察器的方法willSetdidSet呢?,

struct Shape {
    // 寬、邊長
    var width: Int
    // 邊的數量
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetSide", oldValue, side)
        }
    }
    // 周長
    var girth: Int {
        set {
            // 邊長 = 周長 / 邊數
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            // 周長 = 邊長 * 邊數
            return width * side
        }
    }
    func show() {
        print("width=\(width), side=\(side), girth=\(girth)")
        
    }
}
func test(_ num: inout Int) {
    print("test");
    num = 8
}
var s = Shape(width: 10, side: 4)

test(&s.side) //這裏打斷點
複製代碼

輸出

test
willSetSide 8
didSetSide 4 8
複製代碼

查看彙編

關鍵彙編代碼分析

//全局變量0x44e4(%rip)的地址給寄存器rdi,地址是testSwift.Shape + 8也就是size的地址
0x100000f9d <+109>: movq   0x44e4(%rip), %rax        ; testSwift.s : testSwift.Shape + 8

//size的地址給局部變量-0x28(%rbp)
0x100000fa4 <+116>: movq   %rax, -0x28(%rbp)

//局部變量-0x28(%rbp)的值給寄存器 %rdi
0x100000fa8 <+120>: leaq   -0x28(%rbp), %rdi

//調用test函數,寄存器 %rdi做爲參數傳入
0x100000fac <+124>: callq  0x100001930               ; testSwift.test(inout Swift.Int) -> () at main.swift:44

//此時已經修改完了局部變量 -0x28(%rbp)對應的值 並把局部變量 -0x28(%rbp)的值傳給rdi,
0x100000fb1 <+129>: movq   -0x28(%rbp), %rdi
0x100000fb5 <+133>: leaq   0x44c4(%rip), %r13        ; testSwift.s : testSwift.Shape


//從截圖中也能夠看到此時%rdi裏面是8,也就是 test函數中的  num = 8
0x100000fbc <+140>: callq  0x100001240               ; testSwift.Shape.side.setter : Swift.Int at <compiler-generated>
複製代碼

testSwift.Shape.side.setter函數中,調用side.willsetside.didset

小結

對於帶有屬性觀察器的存儲屬性size

  • 首先把size的地址放在一個局部變量中
  • 而後調用test方法,把局部變量的值修改
  • 再把局部變量傳入到setter方法中,真正的修改計算屬性size

inout 修改計算屬性girth

分析

首先分析一下,應該和前面存儲屬性不同的,由於計算屬性girth沒有本身的內存地址,

struct Shape {
    // 寬、邊長
    var width: Int
    // 邊的數量
    var side: Int {
        willSet {
            print("willSetSide", newValue)
        }
        didSet {
            print("didSetSide", oldValue, side)
        }
    }
    // 周長
    var girth: Int {
        set {
            // 邊長 = 周長 / 邊數
            width = newValue / side
            print("setGirth", newValue)
        }
        get {
            print("getGirth")
            // 周長 = 邊長 * 邊數
            return width * side
        }
    }
    func show() {
        print("width=\(width), side=\(side), girth=\(girth)")
        
    }
}
func test(_ num: inout Int) {
    print("test");
    num = 8
}
var s = Shape(width: 10, side: 4)

test(&s.girth) //這裏打斷點
複製代碼

輸出

getGirth
test
setGirth 8
複製代碼

查看彙編

彙編分析

關鍵彙編代碼分析

// 調用testSwift.Shape.girth.getter 方法,返回值放在rax中
0x100000fab <+123>: callq  0x100001580               ; testSwift.Shape.girth.getter : Swift.Int at main.swift:33

// 把getter的返回值放在 局部變量-0x28(%rbp)中
0x100000fb0 <+128>: movq   %rax, -0x28(%rbp)

//局部變量-0x28(%rbp)的地址值 放在寄存器rdi
0x100000fb4 <+132>: leaq   -0x28(%rbp), %rdi

//寄存器rdi的地址值傳到testSwift.test函數中,進行修改
0x100000fb8 <+136>: callq  0x100001930               ; testSwift.test(inout Swift.Int) -> () at main.swift:44

// 局部變量-0x28(%rbp)的值,傳到寄存器rdi中
0x100000fbd <+141>: movq   -0x28(%rbp), %rdi


0x100000fc1 <+145>: leaq   0x44b8(%rip), %r13        ; testSwift.s : testSwift.Shape

// 寄存器rdi裏面放的是局部變量-0x28(%rbp)的值 傳入到Shape.girth.setter中
0x100000fc8 <+152>: callq  0x1000012f0               ;  testSwift.Shape.girth.setter : Swift.Int at main.swift:28
複製代碼

小結

由於計算屬性自己沒有地址值,因此過程略顯複雜

對於inout修改計算屬性girth

  • 首先調用getter方法,把返回值放在一個局部變量中
  • 而後調用test方法,把局部變量的值修改
  • 再把局部變量傳入到setter方法中,真正的修改計算屬性girth

總結

針對本文代碼的總結

輸入輸出參數inout 本質就是引用傳遞,也就是地址傳遞,根據傳過來的地址,修改對應的值。針對不一樣的狀況,其餘處理不一樣,

  • 普通存儲屬性,直接把地址值傳過來修改就能夠了。
  • 對於inout帶有屬性觀察器的存儲屬性size
    • 首先把size的地址放在一個局部變量中
    • 而後調用test方法,把局部變量的值修改
    • 再把局部變量傳入到setter方法中,真正的修改計算屬性size
  • 對於inout修改計算屬性girth
    • 首先調用getter方法,把返回值放在一個局部變量中
    • 而後調用test方法,把局部變量的值修改
    • 再把局部變量傳入到setter方法中,真正的修改計算屬性girth

針對inout的總結

  • 若是實參有物理內存地址,且沒有設置屬性觀察器
    • 直接將實參的內存地址傳入函數(實參進行引用傳遞)
    • 若是實參是計算屬性 或者 設置了屬性觀察器
  • 採起了Copy In Copy Out的作法
    • 調用該函數時,先複製實參的值,產生副本【get】
    • 將副本的內存地址傳入函數(副本進行引用傳遞),在函數內部能夠修改副本的值
    • 函數返回後,再將副本的值覆蓋實參的值【set】
  • 總結:inout的本質就是引用傳遞(地址傳遞)

參考資料:

Swift官方源碼

從入門到精通Swift編程

更多資料,歡迎關注我的公衆號,不定時分享各類技術文章。

相關文章
相關標籤/搜索