指針是一個代表著某個記憶體地址的值, 這個記憶體地址往往是在記憶體中存儲的另一個變數的值的起始位置. Go語言對指針的支持介於Java語言和 C/C++ 語言之間, 它既沒有像Java那樣取消了代碼對指針的直接操作的能力, 也避免了 C/C++ 中由於對指針的濫用而造成的安全和可靠性問題. 指針地址和變數 ...
指針是一個代表著某個記憶體地址的值, 這個記憶體地址往往是在記憶體中存儲的另一個變數的值的起始位置.
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位
為例.
其中, 數據區保存的是初始化後的數據.
上面的代碼都存儲在棧區. 一般 make()
或者 new()
出來的都存儲在堆區
接下來, 我們來瞭解一個新的概念: 棧幀.
棧幀: 用來給函數運行提供記憶體空間, 取記憶體於 stack
上.
當函數調用時, 產生棧幀; 函數調用結束, 釋放棧幀.
那麼棧幀用來存放什麼?
- 局部變數
- 形參
- 記憶體欄位描述值
其中, 形參與局部變數存儲地位等同
當我們的程式運行時, 首先運行 main()
, 這時就產生了一個棧幀.
當運行到 var x int = 99
時, 就會在棧幀裡面產生一個空間.
同理, 運行到 var p *int = &x
時也會在棧幀里產生一個空間.
如下圖所示:
我們增加一個函數, 再來研究一下.
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()
產生的棧幀還沒有結束.
當 test()
運行完畢時, 就會釋放掉這個棧幀.
空指針與野指針
空指針: 未被初始化的指針.
var p *int
這時如果我們想要對其取值操作 *p
, 會報錯.
野指針: 被一片無效的地址空間初始化.
var p *int = 0xc00000a0c8
指針變數的記憶體存儲
表達式 new(T)
將創建一個 T
類型的匿名變數, 所做的是為 T
類型的新值分配並清零一塊記憶體空間, 然後將這塊記憶體空間的地址作為結果返回, 而這個結果就是指向這個新的 T
類型值的指針值, 返回的指針類型為 *T
.
new()
創建的記憶體空間位於heap上, 空間的預設值為數據類型的預設值. 如: p := new(int)
則 *p
為 0
.
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
中的 *p
與 fmt.Println(*p)
中的 *p
是一樣的嗎?
大家先思考一下, 然後先來看一個簡單的例子:
var x int = 10
var y int = 20
x = y
好, 大家思考一下上面代碼中, var y int = 20
中的 y
與 x = 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()
時, 系統在棧區產生一個棧幀, 該棧幀里有 x
和 y
兩個變數.
當運行 swap()
時, 系統在棧區產生一個棧幀, 該棧幀裡面有 x
和 y
兩個變數.
運行 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
這裡並沒有違反 函數傳參永遠都是值傳遞
這句話, 只不過這個時候這個值為地址值.
這個時候, x
與 y
的值就完成了交換.
我們來分析一下這個過程.
首先運行 main()
後創建一個棧幀, 裡面有 x
y
兩個變數.
運行 swap2()
時, 同樣創建一個棧幀, 裡面有 a
b
兩個變數.
註意這個時候, a
和 b
中存儲的值是 x
和 y
的地址.
當運行到 *a, *b = *b, *a
時, 左邊的 *a
代表的是 x
的記憶體地址, 右邊的 *b
代表的是 y
的記憶體地址中的內容. 所以這個時候, main()
中的 x
就被替換掉了.
所以, 這是在 swap2()
中操作 main()
里的變數值.
現在 swap2()
再釋放也沒有關係了, 因為 main()
里的值已經被改了.