当前位置:   article > 正文

【go从入门到精通】一文把指针整的明明白白

【go从入门到精通】一文把指针整的明明白白

什么是指针?

  • 指针是一种存储变量内存地址的变量。
  • 区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,也不嫩进行指针类型转换,是安全指针。 

指针有什么用?

           在Go语言中,通过指针,我们可以直接访问和修改变量的值,而不需要拷贝变量本身,这样可以提高程序的效率。指针在Go语言中有以下几个作用:

  1. 传递变量的地址:指针可以作为函数参数,将变量的地址传递给函数,这样函数就可以直接修改变量的值。

  2. 动态分配内存:通过指针可以在运行时动态分配内存,这样程序就可以根据需要动态地创建和销毁变量。

  3. 访问复杂数据结构:指针可以用于访问复杂的数据结构,如链表、树等,通过指针可以方便地遍历和修改这些数据结构。

  4. 函数返回值:指针可以作为函数的返回值,这样函数就可以返回一个指向变量的指针,而不是变量本身,这样可以避免变量拷贝和内存分配的开销。

Go语言中的指针

        Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int*int64*string等。

基本语法

  • var ptr *int: 声明指针变量ptr,用于指向一个int类型变量的地址。
  • &a获取变量a的内存地址,返回一个指向该地址的指针。
  • *ptr: 读取ptr指针指向地址的值,这个操作称为“解引用”。
  • *ptr = 100: 将100赋值给ptr指向的变量。

Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。

指针地址和指针类型

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

取变量指针的语法如下:

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

其中:

  1. v:代表被取地址的变量,类型为T
  2. ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。

举个例子:

  1. package main
  2. import "fmt"
  3. func main() {
  4. a := 10
  5. b := &a
  6. fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc000112068
  7. fmt.Printf("b:%p type:%T\n", b, b) // b:0xc000112068 type:*int
  8. fmt.Println(&b) // 0xc000114020
  9. }

我们来看一下b := &a的图示:

指针取值

在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。

  1. func main() {
  2. //指针取值
  3. a := 10
  4. b := &a // 取变量a的地址,将指针保存到b中
  5. fmt.Printf("type of b:%T\n", b)
  6. c := *b // 指针取值(根据指针去内存取值)
  7. fmt.Printf("type of c:%T\n", c)
  8. fmt.Printf("value of c:%v\n", c)
  9. }

输出如下:

  1. type of b:*int
  2. type of c:int
  3. value of c:10

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

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

    1.对变量进行取地址(&)操作,可以获得这个变量的指针变量。
    2.指针变量的值是指针地址。
    3.对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

指针传值示例:

  1. func modify1(x int) {
  2. x = 100
  3. }
  4. func modify2(x *int) {
  5. *x = 100
  6. }
  7. func main() {
  8. a := 10
  9. modify1(a)
  10. fmt.Println(a) // 10
  11. modify2(&a)
  12. fmt.Println(a) // 100
  13. }

对于任何一个变量 var, 如下表达式都是正确的:var == *(&var)

注意事项:

你不能得到一个数字或常量的地址,下面的写法是错误的。

例如:

  1. const i = 5
  2. ptr := &i // error: cannot take the address of i
  3. ptr2 := &10 // error: cannot take the address of 10

所以说,Go 语言和 C、C++ 以及 D 语言这些低级(系统)语言一样,都有指针的概念。

但是对于经常导致 C 语言内存泄漏继而程序崩溃的指针运算(所谓的指针算法,如:pointer+2,移动指针指向字符串的字节数或数组的某个位置)是不被允许的。

Go 语言中的指针保证了内存安全,更像是 Java、C# 和 VB.NET 中的引用。

因此 c = *p++ 在 Go 语言的代码中是不合法的。

指针的一个高级应用是你可以传递一个变量的引用(如函数的参数),这样不会传递变量的拷贝。指针传递是很廉价的,只占用 4 个或 8 个字节。当程序在工作中需要占用大量的内存,或很多变量,或者两者都有,使用指针会减少内存占用和提高效率。被指向的变量也保存在内存中,直到没有任何指针指向它们,所以从它们被创建开始就具有相互独立的生命周期。

空指针

  • 当一个指针被定义后没有分配到任何变量时,它的值为 nil
  • 空指针的判断
  1. package main
  2. import "fmt"
  3. func main() {
  4. var p *string
  5. fmt.Println(p)
  6. fmt.Printf("p的值是%v\n", p)
  7. if p != nil {
  8. fmt.Println("非空")
  9. } else {
  10. fmt.Println("空值")
  11. }
  12. }

如我们所见,在大多数情况下 Go 语言可以使程序员轻松创建指针,并且隐藏间接引用,如:自动反向引用。但是对一个空指针的反向引用是不合法的,并且会使程序崩溃:

  1. package main
  2. func main() {
  3. var p *int = nil
  4. *p = 0
  5. }

panic: runtime error: invalid memory address or nil pointer dereference

new和make

我们先来看一个例子:

  1. func main() {
  2. var a *int
  3. *a = 100
  4. fmt.Println(*a)
  5. var b map[string]int
  6. b["测试"] = 100
  7. fmt.Println(b)
  8. }

执行上面的代码会引发panic,为什么呢? 在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存

new

new是一个内置的函数,它的函数签名如下:

    func new(Type) *Type

其中,

  1. 1.Type表示类型,new函数只接受一个参数,这个参数是一个类型
  2. 2.*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:

  1. func main() {
  2. a := new(int)
  3. b := new(bool)
  4. fmt.Printf("%T\n", a) // *int
  5. fmt.Printf("%T\n", b) // *bool
  6. fmt.Println(*a) // 0
  7. fmt.Println(*b) // false
  8. }

本节开始的示例代码中var a *int只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:

  1. func main() {
  2. var a *int
  3. a = new(int)
  4. *a = 10
  5. fmt.Println(*a)
  6. }

make

make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:

func make(t Type, size ...IntegerType) Type

make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。这个我们在上一章中都有说明,关于channel我们会在后续的章节详细说明。

本节开始的示例中var b map[string]int只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值:

  1. func main() {
  2. var b map[string]int
  3. b = make(map[string]int, 10)
  4. b["测试"] = 100
  5. fmt.Println(b)
  6. }

new与make的区别

  1. 1.二者都是用来做内存分配的。
  2. 2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
  3. 3.new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

new和&

new和&均可获取地址,也就是指针

  1. package main
  2. import "fmt"
  3. type Player struct {
  4. name string
  5. id string
  6. level int
  7. }
  8. func main() {
  9. p1 := new(Player) //使用new创建一个p1对象,同时获得Player的指针
  10. p2 := &Player{name: "12", id: "0", level: 1} //使用&创建一个p3对象,同时获得Player的指针
  11. p3 := Player{name: "12", id: "0", level: 1} //直接在栈空间创建一个p3对象,同时获得Player的指针
  12. fmt.Println("%p,%p,%p", &p1, &p2, (&p3))
  13. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/243602
推荐阅读
相关标签
  

闽ICP备14008679号