指针

本节我们介绍下Golang中的指针,并了解下Golang中的指针跟其他编程语言比如C、C++之间的区别。

什么是指针

指针是一个存储了其他变量的内存地址的变量。这句话有点绕,怎么理解呢?其实很简单。指针其实也是一个变量,只要是变量就会存储某个值,指针存储的值比较特殊,是另外一个变量的内存地址。

看一下前面这张示例图,变量b存储的值为156,其在内存中的地址为0x1040a124。变量a存储的值是变量b的地址。因此这里变量a指向变量b,变量a是变量b的指针。

声明一个指针变量

一个指向T类型数据的指针变量声明格式为*T。 我们写个函数来声明一个指针变量。

package main

import "fmt"

func main() {
    b := 255
    var a *int = &b
    fmt.Printf("Type of a is %T\n", a)
    fmt.Println("Address of b is", a)
}

Go Playground在线运行 这里的&运算符用来获取一个变量的地址。我们将变量b的地址赋值给指针变量a,其数据类型为*int。这时指针a指向变量b。当我们打印指针变量a的值时,其实打印的是变量b的地址。该程序输出如下:

Type of a is *int
Address of b is 0x414020

变量b可能存在于内存中的任何地址,因此你的打印结果可能跟我的不同。

指针变量的零值

指针变量的零值为nil

package main

import "fmt"

func main() {
    a := 25
    var b *int
    if b == nil {
        fmt.Println("b is", b)
        b = &a
        fmt.Println("b after initialization is", b)
    }
}

Go Playground在线运行 这里指针变量b的初始值为nil,然后我们给其赋值变量a的地址。执行结果如下:

b is <nil>
b after initialization is 0x414020

通过new函数来创建指针

Golang提供了一个便捷的new函数用来创建指针。new函数的入参为类型T,返回值为一个指向该类型零值变量的指针。有点绕,我们举个例子看下。

package main

import "fmt"

func main() {
    size := new(int)
    fmt.Printf("Size value is %d, type is %T, address is %v\n", *size, size, size)
    *size = 85
    fmt.Println("New size value is", *size)
}

Go Playground在线运行 这里我们通过new函数创建了一个指向一个临时的int类型数据的指针,该临时数据分配的值为对应类型的零值,比如这里即为int类型的零值0。该程序执行结果如下:

Size value is 0, type is *int, address is 0x414020
New size value is 85

指针解除引用

所谓的指针解引用指的是访问指针指向的变量的值。指针a解引用的语法为*a

下面通过一个例子来了解下:

package main

import "fmt"

func main() {
    b := 255
    a := &b
    fmt.Println("address of b is", a)
    fmt.Println("value of b is", *a)
}

Go Playground在线运行 这里我们通过指针a解引用来访问其指向的变量b的值。执行结果如下:

address of b is 0x414020
value of b is 255

下面修改下前面的程序,我们通过指针来修改变量b的值:

package main

import "fmt"

func main() {
    b := 255
    a := &b
    fmt.Println("address of b is", a)
    fmt.Println("value of b is", *a)
    *a++
    fmt.Println("new value of b is", b)
}

Go Playground在线运行 这里我们通过*a++将指针a指向的变量加1,由于a指向的变量为b,因此b变量的值变为256。该程序输出如下:

address of b is 0x414020
value of b is 255
new value of b is 256

指针作为函数入参

package main

import "fmt"

func change(p *int) {
    *p = 55
}

func main() {
    a := 58
    fmt.Println("value of a before function call is", a)
    b := &a
    change(b)
    fmt.Println("value of a after function call is", a)
}

Go Playground在线运行 这里我们将指向变量a的指针变量b作为入参传递给函数changechange函数内部通过指针解引用改变了变量a的值。输出结果为:

value of a before function call is 58
value of a after function call is 55

指针作为函数返回值

Golang中允许返回指向局部变量的指针,如果一个指向局部变量的指针作为函数返回值,那么Golang编译器会在堆内存上分配该局部变量。

package main

import "fmt"

func hello() *int {
    i := 5
    return &i
}

func main() {
    d := hello()
    fmt.Println("value of d", *d)
}

Go Playground在线运行 这里函数hello将局部变量i的指针作为函数返回值。在其他诸如C和C++之类的编程语言中这种写法是错误的,因为变量i是局部变量,一旦函数返回,局部变量就访问不到了。但是Golang会在局部变量初始化时检查下是否会在函数外部被引用,比如这里的局部变量i,其指针作为函数返回值返回出去,因此会在函数外部引用,这是Golang就将该局部变量分配在堆内存中。该程序执行结果为:

value of d 5

尽量避免数组指针,多用切片

如果我们把一个指向数组的指针作为函数入参,那么可以在函数内部通过该指针来修改数组内部值,并且该修改在函数外也是可见的。

package main

import "fmt"

func modify(arrP *[3]int) {
    (*arrP)[0] = 90
}

func main() {
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

Go Playground在线运行 这里将数组a的指针作为函数modify的入参,并通过该指针来将其指向的数组的第一个元素的值改为90。输出为:[90 90 91]

(*a)[x]可以简写为a[x],因此前面程序中的(*arrP)[0]可以简写为arrP[0]

package main

import "fmt"

func modify(arrP *[3]int) {
    arrP[0] = 90
}

func main() {
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

Go Playground在线运行 执行结果不变,输出:[90 90 91]

尽管这种通过数组指针来在函数内部修改数组是行得通的,但是Golang中一般不这么用,Golang中比较惯用的用法一般是使用切片(slice)。

package main

import "fmt"

func modify(sls []int) {
    sls[0] = 90
}

func main() {
    a := [3]int{89, 90, 91}
    modify(a[:])
    fmt.Println(a)
}

Go Playground在线运行 这里我们将一个切片a[:]作为函数modify的入参,然后在函数内部将该切片的第一个元素修改为90。该程序输出:[90 90 91]

因此,我们在Golang中,尽量避免数组指针,而应该多用切片

Golang不支持指针运算

Golang跟C和C++不同,不支持指针运算,这其实在一定程度上降低了Golang这门编程语言的复杂度。

package main

func main() {
    b := [...]int{109, 110, 111}
    p := b
    p++
}

Go Playground在线运行

该程序执行会报错:

invalid operation: p++ (non-numeric type [3]int)

Last updated