Go基礎:指針地址、指針類型、多重指針、指針運算

目錄

到底什麼是指針呢?

指針

指針地址和指針類型

指針取值

指針變量初始化

指針運算符

多重指針

指針運算


到底什麼是指針呢?

內存就是一系列有序列號的存儲單元,變量就是編譯器爲內存地址分配的暱稱,那麼指針是什麼呢?

指針就是一個指向另一個內存地址變量的值

指針指向變量的內存地址,指針就像該變量值的內存地址一樣

我們來看一個代碼片段

func main() {
    a := 200
    b := &a
    *b++
    fmt.Println(a)
}

在 main 函數的第一行,我們定義了一個新的變量 a ,並賦值爲 200。接下來我們定義了一個變量 b ,並將變量 a 的地址賦值給 b 。我們並不知道 a 的準確存儲地址,但是我們依然可以將 a 的地址存儲在變量 b 中。

因爲 Go 強類型的特性,第三行代碼也許是最具干擾性的了,b 包含 a 變量的地址,但是我們想增加存儲在 a 變量中的值。

這樣我們必須取消引用 b ,而是跟隨指針由 b 引用 a。
然後我們將該值加 1 後,存儲回 b 中存儲的內存地址上。

最後一行打印了 a 的值,可以看到 a 的值已經增加爲了 201

指針

Go語言中的函數傳參都是值拷貝,當我們想要修改某個變量的時候,我們可以創建一個指向該變量地址的指針變量

區別於C/C++中的指針,Go語言中的指針不能進行偏移和運算,是安全指針

要搞明白Go語言中的指針需要先知道3個概念:指針地址、指針類型和指針取值

指針地址和指針類型

Go語言中的指針操作非常簡單,只需要記住兩個符號:&(取地址)和*(根據地址取值)

每個變量在運行時都擁有一個地址,這個地址代表變量在內存中的位置。Go語言中使用&字符放在變量前面對變量進行“取地址”操作。

取變量指針的語法如下:

ptr := &v    // v的類型爲T

其中:

v:代表被取地址的變量,類型爲T
ptr:用於接收地址的變量,ptr的類型就爲*T,稱做T的指針類型。*代表指針。

func main() {
    a := 10
    b := &a
    fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
    fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
    fmt.Println(&b)                    // 0xc00000e018
}

指針取值

 取地址操作符&和取值操作符*是一對互補操作符,&取出地址,*根據地址取出地址指向的值。

變量、指針地址、指針變量、取地址、取值的相互關係和特性如下:

1.對變量進行取地址(&)操作,可以獲得這個變量的指針變量。
2.指針變量的值是指針地址。
3.對指針變量進行取值(*)操作,可以獲得指針變量指向的原變量的值。

  • 當一個指針被定義後沒有分配到任何變量時,它的值爲 nil

指針變量初始化

func main() {
    var a *int
    *a = 100
    fmt.Println(*a)

    var b map[string]int
    b["測試"] = 100
    fmt.Println(b)
}


// panic: runtime error: invalid memory address or nil pointer dereference
//[signal 0xc0000005 code=0x1 addr=0x0 pc=0x49a7ca]

執行上面的代碼會引發panic,爲什麼呢?

  •  在Go語言中對於引用類型的變量,我們在使用的時候不僅要聲明它,還要爲它分配內存空間,否則我們的值就沒辦法存儲。
  • 而對於值類型的聲明不需要分配內存空間,是因爲它們在聲明的時候已經默認分配好了內存空間

 Go語言中new和make是內建的兩個函數,主要用來分配內存

func new(Type) *Type
func make(t Type, size ...IntegerType) Type

1. 二者都是用來做內存分配的。
2. make只用於slice、map以及channel的初始化,返回的還是這三個引用類型本身
3. 而new用於類型的內存分配,並且內存對應的值爲類型零值,返回的是指向類型的指針。

指針運算符

1.指針運算符爲左值時,我們可以更新目標對象的狀態;而爲右值時則是爲了獲取目標的狀態。

func main() {
    x := 10
    var p *int = &x  //獲取地址,保存到指針變量
    *p += 20        //用指針間接引用,並更新對象
    println(p, *p)  //輸出指針所存儲的地址,以及目標對象
}

輸出:

0xc000040780 30

2.指針類型支持相等運算符,但不能做加減運算和類型轉換。如果兩個指針指向同一地址,或都爲nil,那麼它們相等。

func main() {
    x := 10
    p := &x

    p++   //編譯報錯 invalid operation: p++ (non-numeric type *int)
    var p2 *int = p+1  //invalid operation: p + 1 (mismatched types *int and int)
    p2 = &x
    println(p == p2)   //指向同一地址
}

可通過unsafe.Pointer將指針轉換爲uintptr後進行加減法運算,但可能會造成非法訪問。


多重指針

指針可以指向任何類型的變量。所以也可以指向另一個指針。以下示例顯示如何創建指向另一個指針的指針:

package main

import "fmt"

func main() {

	var a = 3.141596
	var p = &a
	var pp = &p

	fmt.Println("a = ", a)
	fmt.Println("p = ", p)
	fmt.Println("pp = ", pp)

	fmt.Println("&p = ", &p)
	fmt.Println("&pp = ", &pp)

	fmt.Println("*p = ", *p)
	fmt.Println("*pp = ", *pp)
	fmt.Println("**pp = ", **pp)

	//a =  3.141596
	//p =  0xc00008e060
	//pp =  0xc000090018
	//&p =  0xc000090018
	//&pp =  0xc000090020
	//*p =  3.141596
	//*pp =  0xc00008e060
	//**pp =  3.141596

}

指針運算

在很多 golang 程序中,雖然用到了指針,但是並不會對指針進行加減運算,這和 C 程序是很不一樣的。Golang 的官方入門學習工具(go tour) 甚至說 Go 不支持指針算術。雖然實際上並不是這樣的,但我在一般的 go 程序中,好像確實沒見過指針運算(嗯,我知道你想寫不一般的程序)。

  • 但實際上,go 可以通過 unsafe.Pointer 來把指針轉換爲 uintptr 類型的數字,來實現指針運算。
  • 這裏請注意,uintptr 是一種整數類型,而不是指針類型。

比如:

uintptr(unsafe.Pointer(&p)) + 1

就得到了 &p 的下一個字節的位置。然而,根據 《Go Programming Language》 的提示,我們最好直接把這個計算得到的內存地址轉換爲指針類型:

unsafe.Pointer(uintptr(unsafe.Pointer(&p) + 1))

因爲 go 中是有垃圾回收機制的,如果某種 GC 挪動了目標值的內存地址,以整型來存儲的指針數值,就成了無效的值。

同時也要注意,go 中對指針的 + 1,真的就只是指向了下一個字節,而 C 中 + 1 或者 ++ 考慮了數據類型的長度,會自動指向當前值結尾後的下一個字節(或者說,有可能就是下一個值的開始)。如果 go 中要想實現同樣的效果,可以使用 unsafe.Sizeof 方法:

unsafe.Pointer(uintptr(unsafe.Pointer(&p) + unsafe.Sizeof(p)))

最後,另外一種常用的指針操作是轉換指針類型。這也可以利用 unsafe 包來實現:

var a int64 = 1
(*int8)(unsafe.Pointer(&a))

如果你沒有遇到過需要轉換指針類型的需求,可以看看這個項目(端口掃描工具),其中構建 IP 協議首部的代碼,就用到了指針類型轉換。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章