Calvert's murmur

在 Golang 與指標共舞

2019-11-15

約 3108 字 / 需 17 分鐘閱讀

原文:CalliCoderPlaying with Pointers in Golang

指標是一個變數,用來儲存另一個變數的記憶體位址。疑惑嗎?讓我來解釋一下。

首先讓我們來了解什麼是變數。當我們在撰寫任何程式時,我們需要在記憶體中儲存一些資料。資料儲存在記憶體中的特定位址。記憶體位址看起來會像 0xAFFFF(這是以十六進制來表示記憶體位址)。

現在,要存取資料的話,我們需要知道資料的儲存位址。我們可以追蹤所有與我們的程式相關的儲存資料的記憶體位址。但是,想像一下要記住所有記憶體位址並使用它們存取資料有多麼困難。

這就是為什麼我們有變數的概念。變數只是為儲存資料的記憶體位置所取的一個方便的名稱。

指標也是一個變數。但它是一種特殊的變數,因為它儲存的資料不是一般的數值,如簡單的整數或字串,它是另一個變數的記憶體位址。

在上面的例子中,指標 p 含有數值 0x0001,它是變數 a 的位址。

宣告指標

使用以下語法宣告一個型別為 T 的指標:

// A pointer of type T
var p *T

型別 T 是指標指向的變數的型別。舉例來說,以下是型別為 int 的指標:

// A pointer of type int
var p *int

上面的指標只能儲存 int 變數的記憶體位址。

指標的零值nil。也就是說,任何未初始化的指標都會是 nil。讓我們來看一個完整範例:

package main

import "fmt"

func main() {
	var p *int
	fmt.Println("p = ", p)
}
# Output
p =  <nil>

初始化指標

你可以使用另一個變數的記憶體位址來初始化指標。可以使用 & 運算子取得變數的位址:

var x = 100
var p *int = &x

注意我們如何使用 & 運算子與變數 x 來取得它的位址,然後將位址指派給指標 p

與 Golang 中的其他變數一樣,編譯器也可以推斷出指標變數的型別。因此,你可以省略上面範例中指標 p 的型別宣告,並像這樣撰寫它:

var p = &a

讓我們看一個完整範例來更清楚的了解:

package main

import "fmt"

func main() {
	var a = 5.67
	var p = &a

	fmt.Println("Value stored in variable a = ", a)
	fmt.Println("Address of variable a = ", &a)
	fmt.Println("Value stored in variable p = ", p)
}
# Output
Value stored in variable a =  5.67
Address of variable a =  0xc4200120a8
Value stored in variable p =  0xc4200120a8

解參考指標

你可以對指標使用 * 運算子來存取儲存在指標所指向的變數中的值。這稱為_解參考_或_間接取值_:

package main

import "fmt"

func main() {
	var a = 100
	var p = &a

	fmt.Println("a = ", a)
	fmt.Println("p = ", p)
	fmt.Println("*p = ", *p)
}
# Output
a =  100
p =  0xc4200120a8
*p =  100

你不僅可以使用 * 運算子來存取所指向變數的值,還可以對其進行修改。以下範例透過指標 p 來設定儲存在變數 a 中的值:

package main

import "fmt"

func main() {
	var a = 1000
	var p = &a

	fmt.Println("a (before) = ", a)

	// Changing the value stored in the pointed variable through the pointer
	*p = 2000

	fmt.Println("a (after) = ", a)
}
# Output
a (before) =  1000
a (after) =  2000

使用內建的 new() 函數建立指標

你也可以使用內建的 new() 函數建立指標。new() 函數將型別當作參數,分配足夠的記憶體來容納該型別的值,然後回傳指向該型別的指標。

這有一個範例:

package main

import "fmt"

func main() {
	ptr := new(int) // Pointer to an `int` type
	*ptr = 100

	fmt.Printf("Ptr = %#x, Ptr value = %d\n", ptr, *ptr)
}
# Output
Ptr = 0xc420014058, Ptr value = 100

指標的指標

指標可以指向任何型別的變數。它也可以指向另一個指標。下面的範例展示了如何建立指向另一個指標的指標:

package main

import "fmt"

func main() {
	var a = 7.98
	var p = &a
	var pp = &p

	fmt.Println("a = ", a)
	fmt.Println("address of a = ", &a)

	fmt.Println("p = ", p)
	fmt.Println("address of p = ", &p)

	fmt.Println("pp = ", pp)

	// Dereferencing a pointer to pointer
	fmt.Println("*pp = ", *pp)
	fmt.Println("**pp = ", **pp)
}
# Output
a =  7.98
address of a =  0xc4200120a8
p =  0xc4200120a8
address of p =  0xc42000c028
pp =  0xc42000c028
*pp =  0xc4200120a8
**pp =  7.98

Go 沒有指標運算

如果你使用過 C/C++,那麼你必須知道這些語言支援指標運算。
舉例來說,你可以遞增/遞減指標來移動到下一個/上一個記憶體位址。
你可以對指標增加或減去一個整數值,你也可以使用關係運算子 ==<> 等比較兩個指標。

但是 Go 不支援對指標進行此類算術運算。任何此類運算都會導致編譯時期錯誤:

package main

func main() {
	var x = 67
	var p = &x

	var p1 = p + 1 // Compiler Error: invalid operation
}

但是,你可以使用 == 運算子比較兩個相同型別的指標是否相等。

package main

import "fmt"

func main() {
	var a = 75
	var p1 = &a
	var p2 = &a

	if p1 == p2 {
		fmt.Println("Both pointers p1 and p2 point to the same variable.")
	}
}

結論

我希望你了解什麼是指標、如何宣告和初始化指標,以及如何解參考指標。

Tags: Golang