目錄
到底什麼是指針呢?
內存就是一系列有序列號的存儲單元,變量就是編譯器爲內存地址分配的暱稱,那麼指針是什麼呢?
指針就是一個指向另一個內存地址變量的值
指針指向變量的內存地址,指針就像該變量值的內存地址一樣
我們來看一個代碼片段
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 協議首部的代碼,就用到了指針類型轉換。