Golang 指針

指針是一個表明着某個內存地址的值, 這個內存地址每每是在內存中存儲的另外一個變量的值的起始位置.git

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 的地址.指針

因此, 指針就是地址, 指針變量就是存儲地址的變量.code

接着, 咱們更改 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() 裏的值已經被改了.

李培冠博客

歡迎訪問個人我的網站:

李培冠博客:lpgit.com

相關文章
相關標籤/搜索