golang有指針。Go語言對指針的支持介于Java語言和C/C++語言之間,它既沒有像Java那樣取消了代碼對指針的直接操作的能力,也避免了C/C++中由于對指針的濫用而造成的安全和可靠性問題。
本教程操作環(huán)境:windows10系統(tǒng)、GO 1.11.2、thinkpad t480電腦。
指針是一個代表著某個內存地址的值,這個內存地址往往是在內存中存儲的另一個變量的值的起始位置。
指針地址和變量空間
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
去訪問.
繼續(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) }
可以發(fā)現, x
與 *p
的結果一樣的。
其中, *p
稱為 解引用
或者 間接引用
。
*p = 999
是通過借助 x
變量的地址,來操作 x
對應的空間。
不管是 x
還是 *p
, 我們操作的都是同一個空間。
推薦學習:Golang教程
棧幀的內存布局
首先, 先來看一下內存布局圖, 以 32位
為例.
其中, 數據區(qū)保存的是初始化后的數據.
上面的代碼都存儲在棧區(qū). 一般 make()
或者 new()
出來的都存儲在堆區(qū)
接下來, 我們來了解一個新的概念: 棧幀.
棧幀: 用來給函數運行提供內存空間, 取內存于 stack
上.
當函數調用時, 產生棧幀; 函數調用結束, 釋放棧幀.
那么棧幀用來存放什么?
- 局部變量
- 形參
- 內存字段描述值
其中, 形參與局部變量存儲地位等同
當我們的程序運行時, 首先運行 main()
, 這時就產生了一個棧幀.
當運行到 var x int = 99
時, 就會在棧幀里面產生一個空間.
同理, 運行到 var p *int = &x
時也會在棧幀里產生一個空間.
如下圖所示:
我們增加一個函數, 再來研究一下.
package mainimport "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)
時, 會繼續(xù)產生一個棧幀, 這時 main()
產生的棧幀還沒有結束.
當 test()
運行完畢時, 就會釋放掉這個棧幀.
空指針與野指針
空指針: 未被初始化的指針.
var p *int
這時如果我們想要對其取值操作 *p
, 會報錯.
野指針: 被一片無效的地址空間初始化.
var p *int = 0xc00000a0c8
指針變量的內存存儲
表達式 new(T)
將創(chuàng)建一個 T
類型的匿名變量, 所做的是為 T
類型的新值分配并清零一塊內存空間, 然后將這塊內存空間的地址作為結果返回, 而這個結果就是指向這個新的 T
類型值的指針值, 返回的指針類型為 *T
.
new()
創(chuàng)建的內存空間位于heap上, 空間的默認值為數據類型的默認值. 如: p := new(int)
則 *p
為 0
.
package mainimport "fmt"func main(){ p := new(int) fmt.Println(p) fmt.Println(*p)}
這時 p
就不再是空指針或者野指針.
我們只需使用 new()
函數, 無需擔心其內存的生命周期或者怎樣將其刪除, 因為Go語言的內存管理系統(tǒng)會幫我們打理一切.
接著我們改一下*p
的值:
package mainimport "fmt"func main(){ p := new(int) *p = 1000 fmt.Println(p) fmt.Println(*p)}
這個時候注意了, *p = 1000
中的 *p
與 fmt.Println(*p)
中的 *p
是一樣的嗎?
大家先思考一下, 然后先來看一個簡單的例子:
var x int = 10var y int = 20x = 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 = 1000fmt.Println(*p)
所以, *p = 1000
的意思是把1000寫到 *p
的內存中去;
fmt.Println(*p)
是把 *p
的內存空間中存儲的數據值打印出來.
所以這兩者是不一樣的.
如果我們不在main()創(chuàng)建會怎樣?
func foo() { p := new(int) *p = 1000}
我們上面已經說過了, 當運行 foo()
時會產生一個棧幀, 運行結束, 釋放棧幀.
那么這個時候, p
還在不在?
p
在哪? 棧幀是在棧上, 而 p
因為是 new()
生成的, 所以在 堆
上. 所以, p
沒有消失, p
對應的內存值也沒有消失, 所以利用這個我們可以實現傳地址.
對于堆區(qū), 我們通常認為它是無限的. 但是無限的前提是必須申請完使用, 使用完后立即釋放.
函數的傳參
明白了上面的內容, 我們再去了解指針作為函數參數就會容易很多.
傳地址(引用): 將地址值作為函數參數傳遞.
傳值(數據): 將實參的值拷貝一份給形參.
無論是傳地址還是傳值, 都是實參將自己的值拷貝一份給形參.只不過這個值有可能是地址, 有可能是數據.
所以, 函數傳參永遠都是值傳遞.
了解了概念之后, 我們來看一個經典的例子:
package mainimport "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: 10main x: 10 y: 20
我們先來簡單分析一下為什么不一樣.
首先當運行 main()
時, 系統(tǒng)在棧區(qū)產生一個棧幀, 該棧幀里有 x
和 y
兩個變量.
當運行 swap()
時, 系統(tǒng)在棧區(qū)產生一個棧幀, 該棧幀里面有 x
和 y
兩個變量.
運行 x, y = y, x
后, 交換 swap()
產生的棧幀里的 xy
值. 這時 main()
里的 xy
沒有變.
swap()
運行完畢后, 對應的棧幀釋放, 棧幀里的x
y
值也隨之消失.
所以, 當運行 fmt.Println("main x: ", x, "y: ", y)
這句話時, 其值依然沒有變.
接下來我們看一下參數為地址值時的情況.
傳地址的核心思想是: 在自己的棧幀空間中修改其它棧幀空間中的值.
而傳值的思想是: 在自己的棧幀空間中修改自己棧幀空間中的值.
注意理解其中的差別.
繼續(xù)看以下這段代碼:
package mainimport "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()
后創(chuàng)建一個棧幀, 里面有 x
y
兩個變量.
運行 swap2()
時, 同樣創(chuàng)建一個棧幀, 里面有 a
b
兩個變量.
注意這個時候, a
和 b
中存儲的值是 x
和 y
的地址.
當運行到 *a, *b = *b, *a
時, 左邊的 *a
代表的是 x
的內存地址, 右邊的 *b
代表的是 y
的內存地址中的內容. 所以這個時候, main()
中的 x
就被替換掉了.
所以, 這是在 swap2()
中操作 main()
里的變量值.
現在 swap2()
再釋放也沒有關系了, 因為 main()
里的值已經被改了.