C語言中文網 目錄

Go語言指針詳解,看這一篇文章就夠了

指針(pointer)概念在 Go 語言中被拆分為兩個核心概念:
  • 類型指針,允許對這個指針類型的數據進行修改。傳遞數據使用指針,而無須拷貝數據。類型指針不能進行偏移和運算。
  • 切片,由指向起始元素的原始指針、元素數量和容量組成。

受益于這樣的約束和拆分,Go 語言的指針類型變量擁有指針的高效訪問,但又不會發生指針偏移,從而避免非法修改關鍵性數據問題。同時,垃圾回收也比較容易對不會發生偏移的指針進行檢索和回收。

切片比原始指針具備更強大的特性,更為安全。切片發生越界時,運行時會報出宕機,并打出堆棧,而原始指針只會崩潰。

C/C++中的指針

說到 C/C++ 中的指針,會讓許多人“談虎色變”,尤其對指針偏移、運算、轉換都非常恐懼。

其實,指針是使 C/C++ 語言有極高性能的根本,在操作大塊數據和做偏移時方便又便捷。因此,操作系統依然使用C語言及指針特性進行編寫。

C/C++ 中指針飽受詬病的根本原因是指針運算和內存釋放。

C/C++ 語言中的裸指針可以自由偏移,甚至可以在某些情況下偏移進入操作系統核心區域。我們的計算機操作系統經常需要更新、修復漏洞的本質,是為解決指針越界訪問所導致的“緩沖區溢出”。

要明白指針,需要知道幾個概念:指針地址、指針類型和指針取值,下面將展開細說。

認識指針地址和指針類型

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

格式如下:

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

其中 v 代表被取地址的變量,被取地址的 v 使用 ptr 變量進行接收,ptr 的類型就為*T,稱做 T 的指針類型。*代表指針。

指針實際用法,通過下面的例子了解:
package main

import (
    "fmt"
)

func main() {
    var cat int = 1
    var str string = "banana"
    fmt.Printf("%p %p", &cat, &str)
}
運行結果:
0xc042052088 0xc0420461b0

代碼說明如下:
  • 第 8 行,聲明整型 cat 變量。
  • 第 9 行,聲明字符串 str 變量。
  • 第 10 行,使用 fmt.Printf 的動詞%p輸出 cat 和 str 變量取地址后的指針值,指針值帶有0x的十六進制前綴。

輸出值在每次運行是不同的,代表 cat 和 str 兩個變量在運行時的地址。

在 32 位平臺上,將是 32 位地址;64 位平臺上是 64 位地址。

提示:變量、指針和地址三者的關系是:每個變量都擁有地址,指針的值就是地址。

從指針獲取指針指向的值

在對普通變量使用&操作符取地址獲得這個變量的指針后,可以對指針使用*操作,也就是指針取值,代碼如下。
package main

import (
    "fmt"
)

func main() {

    // 準備一個字符串類型
    var house = "Malibu Point 10880, 90265"

    // 對字符串取地址, ptr類型為*string
    ptr := &house

    // 打印ptr的類型
    fmt.Printf("ptr type: %T\n", ptr)

    // 打印ptr的指針地址
    fmt.Printf("address: %p\n", ptr)

    // 對指針進行取值操作
    value := *ptr

    // 取值后的類型
    fmt.Printf("value type: %T\n", value)

    // 指針取值后就是指向變量的值
    fmt.Printf("value: %s\n", value)

}
運行結果:
ptr type: *string
address: 0xc0420401b0
value type: string
value: Malibu Point 10880, 90265

代碼說明如下:
  • 第 10 行,準備一個字符串并賦值。
  • 第 13 行,對字符串取地址,將指針保存到 ptr 中。
  • 第 16 行,打印 ptr 變量的類型,類型為 *string。
  • 第 19 行,打印 ptr 的指針地址,每次運行都會發生變化。
  • 第 22 行,對 ptr 指針變量進行取值操作,value 變量類型為 string。
  • 第 25 行,打印取值后 value 的類型。
  • 第 28 行,打印 value 的值。

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

變量、指針地址、指針變量、取地址、取值的相互關系和特性如下:
  • 對變量進行取地址(&)操作,可以獲得這個變量的指針變量。
  • 指針變量的值是指針地址。
  • 對指針變量進行取值(*)操作,可以獲得指針變量指向的原變量的值。

使用指針修改值

通過指針不僅可以取值,也可以修改值。

前面已經使用多重賦值的方法進行數值交換,使用指針同樣可以進行數值交換,代碼如下:
package main

import "fmt"

// 交換函數
func swap(a, b *int) {

    // 取a指針的值, 賦給臨時變量t
    t := *a

    // 取b指針的值, 賦給a指針指向的變量
    *a = *b

    // 將a指針的值賦給b指針指向的變量
    *b = t
}

func main() {

// 準備兩個變量, 賦值1和2
    x, y := 1, 2

    // 交換變量值
    swap(&x, &y)

    // 輸出變量值
    fmt.Println(x, y)
}
運行結果:
2 1

代碼說明如下:
  • 第 6 行,定義一個交換函數,參數為 a、b,類型都為 *int,都是指針類型。
  • 第 9 行,將 a 指針取值,把值(int類型)賦給 t 變量,t 此時也是 int 類型。
  • 第 12 行,取 b 指針值,賦給 a 變量指向的變量。注意,此時*a的意思不是取 a 指針的值,而是“a指向的變量”。
  • 第 15 行,將 t 的值賦給 b 指向的變量。
  • 第 21 行,準備 x、y 兩個變量,賦值 1 和 2,類型為 int。
  • 第 24 行,取出 x 和 y 的地址作為參數傳給 swap() 函數進行調用。
  • 第 27 行,交換完畢時,輸出 x 和 y 的值。

*操作符作為右值時,意義是取指針的值;作為左值時,也就是放在賦值操作符的左邊時,表示 a 指向的變量。其實歸納起來,*操作符的根本意義就是操作指針指向的變量。當操作在右值時,就是取指向變量的值;當操作在左值時,就是將值設置給指向的變量。

如果在 swap() 函數中交換操作的是指針值,會發生什么情況?可以參考下面代碼:
package main

import "fmt"

func swap(a, b *int) {
    b, a = a, b
}

func main() {
    x, y := 1, 2
    swap(&x, &y)
    fmt.Println(x, y)
}
運行結果:
1 2

結果表明,交換是不成功的。上面代碼中的 swap() 函數交換的是 a 和 b 的地址,在交換完畢后,a 和 b 的變量值確實被交換。但和 a、b 關聯的兩個變量并沒有實際關聯。這就像寫有兩座房子的卡片放在桌上一字攤開,交換兩座房子的卡片后并不會對兩座房子有任何影響。

示例:使用指針變量獲取命令行的輸入信息

Go 語言的 flag 包中,定義的指令以指針類型返回。通過學習 flag 包,可以深入了解指針變量在設計上的方便之處。

下面的代碼通過提前定義一些命令行指令和對應變量,在運行時,輸入對應參數的命令行參數后,經過 flag 包的解析后即可通過定義的變量獲取命令行的數據。

獲取命令行輸入:
package main

// 導入系統包
import (
    "flag"
    "fmt"
)

// 定義命令行參數
var mode = flag.String("mode", "", "process mode")

func main() {

    // 解析命令行參數
    flag.Parse()

    // 輸出命令行參數
    fmt.Println(*mode)
}
將這段代碼命名為main.go,然后使用如下命令行運行:

$ go run flagparse.go --mode=fast

命令行輸出結果如下:
fast

代碼說明如下:
  • 第 10 行,通過 flag.String,定義一個 mode 變量,這個變量的類型是 *string。后面 3 個參數分別如下:
    • 參數名稱:在給應用輸入參數時,使用這個名稱。
    • 參數值的默認值:與 flag 所使用的函數創建變量類型對應,String 對應字符串、Int 對應整型、Bool 對應布爾型等。
    • 參數說明:使用 -help 時,會出現在說明中。
  • 第 15 行,解析命令行參數,并將結果寫入創建的指令變量中,這個例子中就是 mode 變量。
  • 第 18 行,打印 mode 指針所指向的變量。

由于之前使用 flag.String 已經注冊了一個 mode 的命令行參數,flag 底層知道怎么解析命令行,并且將值賦給 mode*string 指針。在 Parse 調用完畢后,無須從 flag 獲取值,而是通過自己注冊的 mode 這個指針,獲取到最終的值。代碼運行流程如下圖所示。


圖:命令行參數與變量的關系

創建指針的另一種方法——new() 函數

Go 語言還提供了另外一種方法來創建指針變量,格式如下:

new(類型)

一般這樣寫:
str := new(string)
*str = "ninja"

fmt.Println(*str)
new() 函數可以創建一個對應類型的指針,創建過程會分配內存。被創建的指針指向的值為默認值。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo