1.3. 指针

1.3.1. 指针概念在Go语言中被拆分称为两个核心概念

  • 类型指针,允许这个指针类型的数据进行修改。传递数据使用指针,而无需拷贝数据。类型指针不能进行偏移和运算。

  • 切片,由指向起始元素的原始指针、元素数量和容量组成。

受益于这样的约束和拆分,Go语言的指针类型变量拥有指针的高效访问,但又不会发送指针偏移,从而避免非法修改关键性数据问题。同时,垃圾回收也必将容易对不会发送偏移的指针进行检索和回收。

切片比原始指针具备更强大的特性,更为安全。切片发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。

要明白指针,需要知道几个概念:

指针地址、指针类型、指针取值

1.3.2. 认识指针地址和指针类型

每个变量运行时都拥有一个地址,这个地址代表变量在内存中的位置,Go语言中使用“&”操作符放在变量前面对变量进行“取地址”操作。

格式如下:

pstr := &v    // v的类型为T

其中v代表被取地址的变量,被取地址的v使用ptr变量进行接收,ptr的类型就为"*T",称做T的指针类型。"*"代表指针。

指针实际用法,示例代码:

package main

import "fmt"

func main() {
    var cat int = 1         // 声明整型变量cat
    var str string = "banaba"       //声明字符串str变量
    fmt.Printf("%p %p", &cat, &str) //0xc000062090 0xc00004a1e0     //输出变量cat和变量str取值地址后的指针值
}

提示:变量、指针和地址三者的关系是:每个变量都拥有地址,指针的值就是地址。

1.3.3. 从指针获取指针指向的值

在对普通变量使用"&"操作符取地址获得这个变量的指针后,可以对指针使用"*"操作,进行指针的取值, “*“又被读作”处的值”。

这么记:

ptr := &house       // 取址赋给ptr
value := *ptr       // 根据地址取值赋给value
&   取址              // 处的地址
*   根据地址取值       // 处的值

在函数间传递大数组数据时使用指针

package main

func foo(array [1e6]int)  {
    println("mem chile .....")
}


func foo1(array *[1e6]int)  {
    println("mem chile .....")
}
func main() {
    // 声明一个需要8 MB的数组
    var array [1e6]int
    // 将数组传递给函数foo
    foo(array)

    // 节省了内存,但是会改变共享的内存
    // 将数组的地址传递给函数foo
    foo1(&array)

}

示例代码如下:

package main

import "fmt"

func main() {
    // 准备一个字符串类型
    var house = "Malibu Point 10880, 90265"

    // 对字符串取地址,ptr类型为*string
    ptr := &house

    // 打印ptr的类型
    fmt.Printf("address: %T\n", ptr)        //address: *string
    // 打印ptr的指针地址
    fmt.Printf("address: %p\n", ptr)        //address: 0xc00004a1e0

    // 对指针进行取值操作
    value := *ptr

    // 取值后的类型
    fmt.Printf("value type: %T\n", value)   //value type: string
    // 指针取值后就是指向变量的值
    fmt.Printf("value : %s\n", value)       //value : Malibu Point 10880, 90265
}

取地址操作符“&”和取值操作符“*”是一对互补操作符,“&”取出地址,“*”根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址(&)操作,可以获得这个变量的指针变量。

  • 指针变量的值是指针地址。

  • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

1.3.4. 使用指针修改值

  • 通过指针不仅可以取值,也可以修改值。

指针实现数值交换代码示例:

package main

import "fmt"

// 交换函数
func swap(a, b *int) {                      //定义一个交换函数,参数为a,b.类型都为*int,都是指针类型
    // 取a指针的值,赋给临时变量t
    t := *a                                  // 将a指针的取值,把int类型赋值给t变量。t此时也是int类型
    // 取b指针的值,赋给a指针指向的变量
    *a = *b                                 // 取b指针值,赋给a变量指向的变量。
    // 将a指针的值赋给b指针指向的变量
    *b = t                                  // 将t的值赋给b指向的变量
}

func main() {
    // 准备2个变量,赋值1和2
    x, y := 1, 2
    // 交换变量的值
    swap(&x, &y)
    fmt.Println(x, y)
}

其实归纳起来“*”操作符的根本意义就是操作指针指向的变量。

当操作在右值时, 就是取指向变量的值;

当操作在左值时, 就是将值设置给指向的变量;

1.3.5. 值的传递和引用的传递

package main

import "fmt"

/*值传递:传递的是值的一个拷贝
引用传递:传递的是引用指向的内存地址,会在原值的基础上改变值*/
func test03(n1 *int) {
    *n1 = *n1 + 10
    fmt.Println("test03() n1= ", *n1) //test03() n1=  30
}

func test04(n1 int) {
    n1 = n1 + 20
    fmt.Println("test04 n1= ", n1)  //test04 n1=  50
}

func main() {
    num := 20
    test03(&num)
    fmt.Println("main() num= ", num) //main() num=  30

    num2 := 30
    test04(num2)
    fmt.Println("main() num2= ", num2)  //main() num2=  30
}

示例:使用指针变量获取命令行的输入信息

package main

import (
    "flag"
    "fmt"
    "os"
    "strings"
)

// 定义命令行参数,通过flag.String,定义一个mode变量,这个变量的类型是*string。
var mode  = flag.String("mode","","process mode")
func main() {
    args := os.Args
    if len(args) != 2 {
        flag.PrintDefaults()
        return
    }
    argsflag := strings.Split(args[1],"=")[0]
    if argsflag != "-mode"{
        flag.PrintDefaults()
        return
    }

    // 解析命令行参数
    flag.Parse()
    // 输出命令行参数
    fmt.Println(*mode)

}

/*
D:\go_studay\day3>go run canshu01.go --mode=fast
fast
*/

1.3.6. 创建指针的另一种方法-new()函数

Go语言还提供了另外一种方法来创建指针变量,格式如下:

new(类型)
str := new(string)
*str = "hujianli"
fmt.Println(*str)   //hujianli

new()函数可以创建一个对应类型的指针,创建过程会分配内存。被创建的指针指向的值为默认值。

传值和传引用

package main

import "fmt"

// 传值
func changeIntVal(a int) {
    fmt.Printf("--------changeIntVal函数内:值参数a的内存地址:%p,值为:%v \n ", &a, a)
    a = 90
}

//传引用
func changeIntPtr(a *int) {
    fmt.Printf("--------changeIntPtr函数内:指针参数a的内存地址:%p,值为:%v \n ", &a, a)
    *a = 50
}

func main() {
    a := 10
    fmt.Printf("1.变量a的内存地址:%p,值为:%v \n\n", &a, a)
    fmt.Printf("=========int型变量a的内存地址:%p \n\n", a)
    changeIntVal(a)
    fmt.Printf("2.changgeIntVal函数调用之后,变量a的内存地址:%p,值为:%v \n\n", &a, a)
    changeIntPtr(&a)
    fmt.Printf("3.changgeIntPtr函数调用之后,变量a的内存地址:%p,值为:%v \n\n", &a, a)

}

/*
1.变量a的内存地址:0xc00000a0b8,值为:10

=========int型变量a的内存地址:%!p(int=10)

--------changeIntVal函数内:值参数a的内存地址:0xc00000a0f0,值为:10
2.changgeIntVal函数调用之后,变量a的内存地址:0xc00000a0b8,值为:10

--------changeIntPtr函数内:指针参数a的内存地址:0xc000006030,值为:0xc00000a0b8
3.changgeIntPtr函数调用之后,变量a的内存地址:0xc00000a0b8,值为:50
*/

1.3.7. 指针的核心要点

package main

import "fmt"

/*
指针使用流程如下。
• 定义指针变量。
• 为指针变量赋值。
• 访问指针变量中指向地址的值。
获取指针指向的变量值:在指针类型的变量前加上 * 号(前缀),如*a。
*/

func main() {
    // 定义指针变量
    var zhizhen *int

    // 定义一个int变量
    var id int = 120

    //为指针变量赋值,将id的值赋予zhizhen变量
    zhizhen = &id

    //访问指针变量中指向地址的值。
    fmt.Printf("id变量类型是: %T 值是 %v。\n",id, id)       //id变量类型是: int 值是 120。
    fmt.Printf("&id变量类型是: %T 值是 %v。\n",&id, &id)    //&id变量类型是: *int 值是 0xc00000a0b8。
    fmt.Printf("zhizhen指针变量类型是: %T 指向地址的值是 %v。\n",zhizhen, zhizhen)     //zhizhen指针变量类型是: *int 指向地址的值是 0xc00000a0b8。
    fmt.Printf("*zhizhen指针变量类型是: %T 指向地址的值是 %v。\n",*zhizhen, *zhizhen)  //*zhizhen指针变量类型是: int 指向地址的值是 120。

    fmt.Printf("*&id变量类型是: %T 值是 %v。\n",*&id, *&id)     //*&id变量类型是: int 值是 120。


}

空指针

在Go语言中,当一个指针被定义后没有分配到任何变量时,它的值为nil。nil指针也称为空指针。nil在概念上和其他语言的null、None、NULL一样,都指代零值或空值。

假设指针变量命名为ptr。空指针判断如下

if (prt != nil)     // ptr不是空指针
if (prt == nill)    // ptr是空指针

使用指针

1.通过指针修改变量的数值

package main

import "fmt"

func main() {
    a := 10086
    b := &a
    fmt.Printf("a的地址是:%v\n", b)      // a的地址是:0xc00000a0b8
    fmt.Printf("*b的值是:%v\n",*b)         // *b的值是:10086
    *b++
    fmt.Printf("a的新值是:%v",a)             // a的新值是:10087

}

2.使用指针作为函数的参数

package main

import "fmt"

func main() {
    a := 58
    fmt.Printf("调用函数之前a的值: %v\n", a)            // 调用函数之前a的值: 58
    fmt.Printf("调用函数之前a的内存地址: %v\n", &a)         // 调用函数之前a的内存地址: 0xc000062090

    // 声明b为指针类型的变量,并将a的内存地址赋值给b
    var b *int = &a
    change(b)
    fmt.Printf("调用函数之后a的值:%v\n", a)         // 调用函数之后a的值:15
    fmt.Printf("调用函数之后a的内存地址: %v\n", &a)        // 调用函数之后a的内存地址: 0xc00000a0b8
}

func change(val *int) {
    *val = 15
}

将基本数据类型的指针作为函数的参数,可以实现对传入数据的修改,这是因为指针作为函数的参数只是复制了一个指针,指针指向的内存没有发生改变。