指標的詳細講解

Conan_1996發表於2020-04-15

指標是一個代表著某個記憶體地址的值, 這個記憶體地址往往是在記憶體中儲存的另一個變數的值的起始位置.

Go語言對指標的支援介於Java語言和 C/C++ 語言之間, 它既沒有像Java那樣取消了程式碼對指標的直接操作的能力, 也避免了 C/C++ 中由於對指標的濫用而造成的安全和可靠性問題.

指標地址和變數空間

Go語言保留了指標, 但是與C語言指標有所不同. 主要體現在:

  • 預設值: nil.
  • 操作符 & 取變數地址, * 通過指標訪問目標物件.
  • 不支援指標運算, 不支援 -> 運算子, 直接用 . 訪問目標成員.

先來看一段程式碼:

package main

import "fmt"

func main(){ 
    var x int = 99
    var p *int = &x

    fmt.Println(p)
}

當我們執行到 var x int = 99 時, 在記憶體中就會生成一個空間, 這個空間我們給它起了個名字叫 x, 同時, 它也有一個地址, 例如: 0xc00000a0c8. 當我們想要使用這個空間時, 我們可以用地址去訪問,也可以用我們給它起的名字 x 去訪問.

繼續執行到 var p *int = &x 時, 我們定義了一個指標變數 p , 這個 p 就儲存了變數 x 的地址.

所以, 指標就是地址, 指標變數就是儲存地址的變數.

接著, 我們更改 x 的內容:

package main

import "fmt"

func main() {
    var x int = 99
    var p *int = &x

    fmt.Println(p)

    x = 100

    fmt.Println("x: ", x)
    fmt.Println("*p: ", *p)

    *p = 999

    fmt.Println("x: ", x)
    fmt.Println("*p: ", *p)
}

可以發現, x*p 的結果一樣的.

其中, *p 稱為 解引用 或者 間接引用.

*p = 999 是通過藉助 x 變數的地址, 來操作 x 對應的空間.

不管是 x 還是 *p , 我們操作的都是同一個空間.

棧幀的記憶體佈局

首先, 先來看一下記憶體佈局圖, 以 32位 為例.

image

其中, 資料區儲存的是初始化後的資料.

上面的程式碼都儲存在棧區. 一般 make() 或者 new() 出來的都儲存在堆區

接下來, 我們來了解一個新的概念: 棧幀.

棧幀: 用來給函式執行提供記憶體空間, 取記憶體於 stack 上.

當函式呼叫時, 產生棧幀; 函式呼叫結束, 釋放棧幀.

那麼棧幀用來存放什麼?

  • 區域性變數
  • 形參
  • 記憶體欄位描述值

其中, 形參與區域性變數儲存地位等同

當我們的程式執行時, 首先執行 main(), 這時就產生了一個棧幀.

當執行到 var x int = 99 時, 就會在棧幀裡面產生一個空間.

同理, 執行到 var p *int = &x 時也會在棧幀裡產生一個空間.

如下圖所示:

image

我們增加一個函式, 再來研究一下.

package main

import "fmt"

func test(m int){
    var y int = 66
    y += m
}

func main() {
    var x int = 99
    var p *int = &x

    fmt.Println(p)

    x = 100

    fmt.Println("x: ", x)
    fmt.Println("*p: ", *p)

    test(11)

    *p = 999

    fmt.Println("x: ", x)
    fmt.Println("*p: ", *p)
}

如下圖所示, 當執行到 test(11) 時, 會繼續產生一個棧幀, 這時 main() 產生的棧幀還沒有結束.

image

test() 執行完畢時, 就會釋放掉這個棧幀.

image

空指標與野指標

空指標: 未被初始化的指標.

var p *int

這時如果我們想要對其取值操作 *p, 會報錯.

野指標: 被一片無效的地址空間初始化.

var p *int = 0xc00000a0c8

指標變數的記憶體儲存

表示式 new(T) 將建立一個 T 型別的匿名變數, 所做的是為 T 型別的新值分配並清零一塊記憶體空間, 然後將這塊記憶體空間的地址作為結果返回, 而這個結果就是指向這個新的 T 型別值的指標值, 返回的指標型別為 *T.

new() 建立的記憶體空間位於heap上, 空間的預設值為資料型別的預設值. 如: p := new(int)*p0.

package main

import "fmt"

func main(){
    p := new(int)
    fmt.Println(p)
    fmt.Println(*p)
}

這時 p 就不再是空指標或者野指標.

我們只需使用 new() 函式, 無需擔心其記憶體的生命週期或者怎樣將其刪除, 因為Go語言的記憶體管理系統會幫我們打理一切.

接著我們改一下*p的值:

package main

import "fmt"

func main(){
    p := new(int)

    *p = 1000

    fmt.Println(p)
    fmt.Println(*p)
}

這個時候注意了, *p = 1000 中的 *pfmt.Println(*p) 中的 *p 是一樣的嗎?

大家先思考一下, 然後先來看一個簡單的例子:

var x int = 10
var y int = 20
x = y

好, 大家思考一下上面程式碼中, var y int = 20 中的 yx = y 中的 y 一樣不一樣?

結論: 不一樣

var y int = 20 中的 y 代表的是記憶體空間, 我們一般把這樣的稱之為左值; 而 x = y 中的 y 代表的是記憶體空間中的內容, 我們一般稱之為右值.

x = y 表示的是把 y 對應的記憶體空間的內容寫到x記憶體空間中.

等號左邊的變數代表變數所指向的記憶體空間, 相當於操作.

等號右邊的變數代表變數記憶體空間儲存的資料值, 相當於操作.

在瞭解了這個之後, 我們再來看一下之前的程式碼.

p := new(int)

*p = 1000

fmt.Println(*p)

所以, *p = 1000 的意思是把1000寫到 *p 的記憶體中去;

fmt.Println(*p) 是把 *p的記憶體空間中儲存的資料值列印出來.

所以這兩者是不一樣的.

如果我們不在main()建立會怎樣?

func foo() {
    p := new(int)

    *p = 1000
}

我們上面已經說過了, 當執行 foo() 時會產生一個棧幀, 執行結束, 釋放棧幀.

那麼這個時候, p 還在不在?

p 在哪? 棧幀是在棧上, 而 p 因為是 new() 生成的, 所以在 上. 所以, p 沒有消失, p 對應的記憶體值也沒有消失, 所以利用這個我們可以實現傳地址.

對於堆區, 我們通常認為它是無限的. 但是無限的前提是必須申請完使用, 使用完後立即釋放.

函式的傳參

明白了上面的內容, 我們再去了解指標作為函式引數就會容易很多.

傳地址(引用): 將地址值作為函式引數傳遞.

傳值(資料): 將實參的值拷貝一份給形參.

無論是傳地址還是傳值, 都是實參將自己的值拷貝一份給形參.只不過這個值有可能是地址, 有可能是資料.

所以, 函式傳參永遠都是值傳遞.

瞭解了概念之後, 我們來看一個經典的例子:

package main

import "fmt"

func swap(x, y int){
    x, y = y, x
    fmt.Println("swap  x: ", x, "y: ", y)
}

func main(){
    x, y := 10, 20
    swap(x, y)
    fmt.Println("main  x: ", x, "y: ", y)
}

結果:

swap  x:  20 y:  10
main  x:  10 y:  20

我們先來簡單分析一下為什麼不一樣.

首先當執行 main() 時, 系統在棧區產生一個棧幀, 該棧幀裡有 xy 兩個變數.

當執行 swap() 時, 系統在棧區產生一個棧幀, 該棧幀裡面有 xy 兩個變數.

執行 x, y = y, x 後, 交換 swap() 產生的棧幀裡的 xy 值. 這時 main() 裡的 xy 沒有變.

swap() 執行完畢後, 對應的棧幀釋放, 棧幀裡的x y 值也隨之消失.

所以, 當執行 fmt.Println("main x: ", x, "y: ", y) 這句話時, 其值依然沒有變.

接下來我們看一下引數為地址值時的情況.

傳地址的核心思想是: 在自己的棧幀空間中修改其它棧幀空間中的值.

而傳值的思想是: 在自己的棧幀空間中修改自己棧幀空間中的值.

注意理解其中的差別.

繼續看以下這段程式碼:

package main

import "fmt"

func swap2(a, b *int){
    *a, *b = *b, *a
}

func main(){
    x, y := 10, 20
    swap(x, y)
    fmt.Println("main  x: ", x, "y: ", y)
}

結果:

main  x:  20 y:  10

這裡並沒有違反 函式傳參永遠都是值傳遞 這句話, 只不過這個時候這個值為地址值.

這個時候, xy 的值就完成了交換.

我們來分析一下這個過程.

首先執行 main() 後建立一個棧幀, 裡面有 x y 兩個變數.

執行 swap2() 時, 同樣建立一個棧幀, 裡面有 a b 兩個變數.

注意這個時候, a b 中儲存的值是 x y 的地址.

當執行到 *a, *b = *b, *a 時, 左邊的 *a 代表的是 x 的記憶體地址, 右邊的 *b 代表的是 y 的記憶體地址中的內容. 所以這個時候, main() 中的 x 就被替換掉了.

所以, 這是在 swap2() 中操作 main() 裡的變數值.

現在 swap2() 再釋放也沒有關係了, 因為 main() 裡的值已經被改了.

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章