Go語言 指針

2023-02-16 17:37 更新

雖然Go吸收融合了很多其語言中的各種特性,但是Go主要被歸入C語言家族。其中一個重要的原因就是Go和C一樣,也支持指針。 當然Go中的指針相比C指針有很多限制。本篇文章將介紹指針相關的各種概念和Go指針相關的各種細節(jié)。

內(nèi)存地址

在編程中,一個內(nèi)存地址用來定位一段內(nèi)存。

通常地,一個內(nèi)存地址用一個操作系統(tǒng)原生字(native word)來存儲。 一個原生字在32位操作系統(tǒng)上占4個字節(jié),在64位操作系統(tǒng)上占8個字節(jié)。 所以,32位操作系統(tǒng)上的理論最大支持內(nèi)存容量為4GB(1GB == 230字節(jié)),64位操作系統(tǒng)上的理論最大支持內(nèi)存容量為264Byte,即16EB(EB:艾字節(jié),1EB == 1024PB, 1PB == 1024TB, 1TB == 1024GB)。

內(nèi)存地址的字面形式常用整數(shù)的十六進制字面量來表示,比如?0x1234CDEF?。

以后我們常簡稱內(nèi)存地址為地址。

值的地址

一個值的地址是指此值的直接部分占據(jù)的內(nèi)存的起始地址。在Go中,每個值都包含一個直接部分,但有些值可能還包含一個或多個間接部分,下下章將對此詳述。

什么是指針?

指針是Go中的一種類型分類(kind)。 一個指針可以存儲一個內(nèi)存地址;從地址通常為另外一個值的地址。

和C指針不一樣,為了安全起見,Go指針有很多限制,詳見下面的章節(jié)。

指針類型和值

在Go中,一個無名指針類型的字面形式為?*T?,其中?T?為一個任意類型。類型?T?稱為指針類型?*T?的基類型(base type)。 如果一個指針類型的基類型為?T?,則我們可以稱此指針類型為一個?T?指針類型。

雖然我們可以聲明具名指針類型,但是一般不推薦這么做,因為無名指針類型的可讀性更高。

如果一個指針類型的底層類型是?*T?,則它的基類型為?T?。

如果兩個無名指針類型的基類型為同一類型,則這兩個無名指針類型亦為同一類型。

一些指針類型的例子:

*int  // 一個基類型為int的無名指針類型。
**int // 一個多級無名指針類型,它的基類型為*int。

type Ptr *int // Ptr是一個具名指針類型,它的基類型為int。
type PP *Ptr  // PP是一個具名多級指針類型,它的基類型為Ptr。

指針類型的零值的字面量使用預聲明的?nil?來表示。一個nil指針(常稱為空指針)中不存儲任何地址。

如果一個指針類型的基類型為?T?,則此指針類型的值只能存儲類型為?T?的值的地址。

關于引用(reference)這個術語

在《Go語言101》中,術語“引用”暗示著一個關系。比如,如果一個指針中存儲著另外一個值的地址,則我們可以說此指針值引用著另外一個值;同時另外一個值當前至少有一個引用。 本書對此術語的使用和Go白皮書是一致的。

當一個指針引用著另外一個值,我們也常說此指針指向另外一個值。

如何獲取一個指針值?

有兩種方式來得到一個指針值:

  1. 我們可以用內(nèi)置函數(shù)?new?來為任何類型的值開辟一塊內(nèi)存并將此內(nèi)存塊的起始地址做為此值的地址返回。 假設?T?是任一類型,則函數(shù)調(diào)用?new(T)?返回一個類型為?*T?的指針值。 存儲在返回指針值所表示的地址處的值(可被看作是一個匿名變量)為?T?的零值。
  2. 我們也可以使用前置取地址操作符?&?來獲取一個可尋址的值的地址。 對于一個類型為?T?的可尋址的值?t?,我們可以用?&t?來取得它的地址。?&t?的類型為?*T?。

一般說來,一個可尋址的值是指被放置在內(nèi)存中某固定位置處的一個值(但放置在某固定位置處的一個值并非一定是可尋址的)。 目前,我們只需知道所有變量都是可以尋址的;但是所有常量、函數(shù)返回值和強制轉(zhuǎn)換結果都是不可尋址的。 當一個變量被聲明的時候,Go運行時將為此變量開辟一段內(nèi)存。此內(nèi)存的起始地址即為此變量的地址。

更多可被(或不可被)尋址的值將在以后的文章中逐漸提及。 如果你已經(jīng)對Go比較熟悉,你可以閱讀此條總結來了解在Go中哪些值可以或不可以被尋址。

下一節(jié)中的例子將展示如何獲取一些值的地址。

指針(地址)解引用

我們可以使用前置解引用操作符?*?來訪問存儲在一個指針所表示的地址處的值(即此指針所引用著的值)。 比如,對于基類型為?T?的指針類型的一個指針值?p?,我們可以用?*p?來表示地址?p?處的值。 此值的類型為?T?。?*p?稱為指針?p?的解引用。解引用是取地址的逆過程。

解引用一個nil指針將產(chǎn)生一個恐慌。

下面這個例子展示了如何取地址和解引用。

package main

import "fmt"

func main() {
	p0 := new(int)   // p0指向一個int類型的零值
	fmt.Println(p0)  // (打印出一個十六進制形式的地址)
	fmt.Println(*p0) // 0

	x := *p0              // x是p0所引用的值的一個復制。
	p1, p2 := &x, &x      // p1和p2中都存儲著x的地址。
	                      // x、*p1和*p2表示著同一個int值。
	fmt.Println(p1 == p2) // true
	fmt.Println(p0 == p1) // false
	p3 := &*p0            // <=> p3 := &(*p0)
	                      // <=> p3 := p0
	                      // p3和p0中存儲的地址是一樣的。
	fmt.Println(p0 == p3) // true
	*p0, *p1 = 123, 789
	fmt.Println(*p2, x, *p3) // 789 789 123

	fmt.Printf("%T, %T \n", *p0, x) // int, int
	fmt.Printf("%T, %T \n", p0, p1) // *int, *int
}

下面這張圖描繪了上面這個例子中各個值之間的關系。

指針值

我們?yōu)槭裁葱枰羔槪?/h3>

讓我們先看一個例子:

package main

import "fmt"

func double(x int) {
	x += x
}

func main() {
	var a = 3
	double(a)
	fmt.Println(a) // 3
}

我們本期望上例中的?double?函數(shù)將變量?a?的值放大為原來的兩倍,但是事實證明我們的期望沒有得到實現(xiàn)。 為什么呢?因為在Go中,所有的賦值(包括函數(shù)調(diào)用傳參)過程都是一個值復制過程。 所以在上面的?double?函數(shù)體內(nèi)修改的是變量?a?的一個副本,而沒有修改變量?a?本身。

當然我們可以讓double函數(shù)返回輸入?yún)?shù)的兩倍數(shù),但是此方法并非適用于所有場合。 下面這個例子通過將輸入?yún)?shù)的類型改為一個指針類型來達到同樣的目的。

package main

import "fmt"

func double(x *int) {
	*x += *x
	x = nil // 此行僅為講解目的
}

func main() {
	var a = 3
	double(&a)
	fmt.Println(a) // 6
	p := &a
	double(p)
	fmt.Println(a, p == nil) // 12 false
}

從上例可以看出,通過將?double?函數(shù)的輸入?yún)?shù)的類型改為?*int?,傳入的實參?&a?和它在此函數(shù)體內(nèi)的一個副本?x?都引用著變量?a?。 所以對?*x?的修改等價于對?*p?(也就是變量?a?)的修改。 換句話說,新版本的?double?函數(shù)內(nèi)的操作可以反映到此函數(shù)外了。

當然,在此函數(shù)體內(nèi)對傳入的指針實參的修改?x = nil?依舊不能反映到函數(shù)外,因為此修改發(fā)生在此指針的一個副本上。 所以在?double?函數(shù)調(diào)用之后,局部變量?p?的值并沒有被修改為?nil?。

簡而言之,指針提供了一種間接的途徑來訪問和修改一些值。 雖然很多語言中沒有指針這個概念,但是指針被隱藏其它概念之中。

在Go中返回一個局部變量的地址是安全的

和C不一樣,Go是支持垃圾回收的,所以一個函數(shù)返回其內(nèi)聲明的局部變量的地址是絕對安全的。比如:

func newInt() *int {
	a := 3
	return &a
}

Go指針的一些限制

為了安全起見,Go指針在使用上相對于C指針有很多限制。 通過施加這些限制,Go指針保留了C指針的好處,同時也避免了C指針的危險性。

Go指針不支持算術運算

在Go中,指針是不能參與算術運算的。比如,對于一個指針?p?, 運算?p++?和?p-2?都是非法的。

如果?p?為一個指向一個數(shù)值類型值的指針,?*p++?將被編譯器認為是合法的并且等價于?(*p)++?。 換句話說,解引用操作符?*?的優(yōu)先級都高于自增?++?和自減?--?操作符。

例子:

package main

import "fmt"

func main() {
	a := int64(5)
	p := &a

	// 下面這兩行編譯不通過。
	/*
	p++
	p = (&a) + 8
	*/

	*p++
	fmt.Println(*p, a)   // 6 6
	fmt.Println(p == &a) // true

	*&a++
	*&*&a++
	**&p++
	*&*p++
	fmt.Println(*p, a) // 10 10
}

一個指針類型的值不能被隨意轉(zhuǎn)換為另一個指針類型

在Go中,只有如下某個條件被滿足的情況下,一個類型為T1的指針值才能被顯式轉(zhuǎn)換為另一個指針類型T2

  1. 類型?T1?和?T2?的底層類型必須一致(忽略結構體字段的標簽)。 特別地,如果類型?T1?和?T2?中只要有一個是無名類型并且它們的底層類型一致(考慮結構體字段的標簽),則此轉(zhuǎn)換可以是隱式的。 關于結構體,請參閱下一篇文章。
  2. 類型?T1?和?T2?都為無名類型并且它們的基類型的底層類型一致(忽略結構體字段的標簽)。

比如,

type MyInt int64
type Ta    *int64
type Tb    *MyInt

對于上面所示的這些指針類型,下面的事實成立:

  1. 類型?*int64?的值可以被隱式轉(zhuǎn)換到類型?Ta?,反之亦然(因為它們的底層類型均為?*int64?)。
  2. 類型 ?*MyInt?的值可以被隱式轉(zhuǎn)換到類型?Tb?,反之亦然(因為它們的底層類型均為?*MyInt?)。
  3. 類型?*MyInt?的值可以被顯式轉(zhuǎn)換為類型?*int64?,反之亦然(因為它們都是無名的并且它們的基類型的底層類型均為?int64?)。
  4. 類型?Ta?的值不能直接被轉(zhuǎn)換為類型?Tb?,即使是顯式轉(zhuǎn)換也是不行的。 但是,通過上述三條事實,通過三層顯式轉(zhuǎn)換?Tb((*MyInt)((*int64)(ta)))?,一個類型為?Ta?的值?ta?可以被間接地轉(zhuǎn)換為類型?Tb?。

這些指針類型的任何值都無法被轉(zhuǎn)換到類型?*uint64?。

一個指針值不能和其它任一指針類型的值進行比較

Go指針值是支持(使用比較運算符==!=)比較的。 但是,兩個指針只有在下列任一條件被滿足的時候才可以比較:

  1. 這兩個指針的類型相同。
  2. 其中一個指針可以被隱式轉(zhuǎn)換為另一個指針的類型。換句話說,這兩個指針的類型的底層類型必須一致并且至少其中一個指針類型為無名的(考慮結構體字段的標簽)。
  3. 其中一個并且只有一個指針用類型不確定的?nil?標識符表示。

例子:

package main

func main() {
	type MyInt int64
	type Ta    *int64
	type Tb    *MyInt

	// 4個不同類型的指針:
	var pa0 Ta
	var pa1 *int64
	var pb0 Tb
	var pb1 *MyInt

	// 下面這6行編譯沒問題。它們的比較結果都為true。
	_ = pa0 == pa1
	_ = pb0 == pb1
	_ = pa0 == nil
	_ = pa1 == nil
	_ = pb0 == nil
	_ = pb1 == nil

	// 下面這三行編譯不通過。
	/*
	_ = pa0 == pb0
	_ = pa1 == pb1
	_ = pa0 == Tb(nil)
	*/
}

一個指針值不能被賦值給其它任意類型的指針值

一個指針值可以被賦值給另一個指針值的條件和這兩個指針值可以比較的條件(見上一小節(jié))是一致的。

上述Go指針的限制是可以被打破的

unsafe標準庫包中提供的非類型安全指針(?unsafe.Pointer?)機制可以被用來打破上述Go指針的安全限制。 ?unsafe.Pointer?類型類似于C語言中的?void*?。 但是,通常地,非類型安全指針機制不推薦在Go日常編程中使用。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號