LOADING

正在加载

Go基础学习(伍)——指针

壹 介绍

Go 语言中指针是很容易学习的,Go 语言中使用指针可以更简单的执行一些任务。Go语言中的指针不能像C/C++语言一样进行偏移和运算,是安全指针。要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值

任何程序数据载入内存后,在内存都有他们的地址,这个地址就是指针。而为了保存一个数据在内存中的地址,我们就需要一个变量,这个变量就是指针变量。

个人理解:A是一个变量,这个变量存储了一个值是1,那么如果需要得到A在内存的地址,就需要用另一个变量B存储A在内存的地址,这其中,A是存储1的变量,就是普通的变量,B是存储A这个变量在内存的地址的变量,又叫做指针变量,所以指针变量里面存储的只能是内存地址,所以A、B都能指向1这个值,但是A可以直接获取,B要通过A获取,至此我们可以看出指针本身就是一种引用。

那么如何获取地址和地址的值呢?Go语言中有两个符号:&(取地址)和*(根据地址取值)。即,&是B获取A的地址使用的符号,*是B获取1这个值的符号。

语法格式:

// 取地址
b := &a
// a:代表要被获取地址的变量,类型为T
// b:用于接收该a地址的指针变量,b的类型就为*T,称做T的指针类型。*代表这个变量是指针,存储地址的指针变量。

理解图示:
846d7213e7050dab6241c46bd5a0e295.png
实例:

package main
import "fmt"
func main() {
    var a int = 10
    var b *int = &a
    fmt.Printf("a的值: %v\ta的类型:%T\ta的地址:%p\n", a, a, &a)
    // a的值: 10       a的类型:int    a的地址:0xc000016098
    fmt.Printf("b的值: %v\tb的类型:%T\tb的地址:%p\n", b, b, &b)
    // b的值: 0xc000016098     b的类型:*int   b的地址:0xc000006028
}

贰 指针声明

一个指针变量指向了一个值在内存中的地址。 类似于变量和常量,在使用指针前你需要声明指针。指针声明格式如下:

var 指针变量名 *指针类型
// *号用于指定变量是作为一个指针

以下是有效的指针声明:

var ip *int        /* 指向整型*/
var fp *float32    /* 指向浮点型 */

需要注意:指针类型只能是值类型,不能是引用类型,这是没有意义的。

叁 使用指针

指针使用流程:

graph LR
A[定义指针变量] --> B[为指针变量赋值,用&] --> C[访问指针变量中指向地址的值,用*]

例子:

package main

import "fmt"

func main() {
    var a int = 10
    var b *int = &a
    var c **int = &b
    var d ***int = &c
    // 可以看到对于上面的c是指向指针的指针变量,而d是指向指针的指针的指针变量,大概懂那个意思就行,每一次都加一个*
    fmt.Printf("a的值: %v\ta的类型:%T\ta的地址:%p\n", a, a, &a)
    // a的值: 10       a的类型:int    a的地址:0xc000016098
    fmt.Printf("b的值: %v\tb的类型:%T\tb的地址:%p\n", b, b, &b)
    // b的值: 0xc000016098     b的类型:*int   b的地址:0xc000006028
    fmt.Printf("获取b指针的值: %v\n", *b)
    // 获取b指针的值: 10
    fmt.Printf("c的值: %v\tc的类型:%T\tc的地址:%p\n", c, c, &c)
    // c的值: 0xc000006028     c的类型:**int  c的地址:0xc000006030
    fmt.Printf("获取c指针的值: %v\t获取c指针的指针的值: %v\n", *c, **c)
    // 获取c指针的值: 0xc000016098     获取c指针的指针的值: 10
    fmt.Printf("d的值: %v\td的类型:%T\td的地址:%p\n", d, d, &d)
    // d的值: 0xc000006030     d的类型:***int d的地址:0xc000006038
    fmt.Printf("获取d指针的值: %v\t获取d指针的指针的值: %v\t获取d指针的指针的指针的值: %v\n", *d, **d, ***d)
    // 获取d指针的值: 0xc000006028     获取d指针的指针的值: 0xc000016098       获取d指针的指针的指针的值: 10
}

肆 空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil,nil 指针也称为空指针,nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值,这时候是不可以给该指针常用类型的值的,如:int、float、bool、string等,指针变量只能存储地址

package main
import "fmt"
func main() {
   var ptr *int
   fmt.Printf("ptr 的值为 : %x\n", ptr  )
    //ptr 的值为 : 0
}

伍 指针数组

需要将数组保存到指针中,我们可以对整型指针数组进行以下声明:

var ptr [MAX]*int;
// ptr:是指针数组
// MAX是指针数组的数量
// *int:指针数组的类型

ptr 为整型指针数组。因此每个元素都指向了一个值。以下实例的三个整数将存储在指针数组中:

package main
import "fmt"
// 定义一个MAX,是方便统一管理数组的数量
const MAX int = 3
func main() {
   a := []int{10,100,200}
   var i int
   var ptr [MAX]*int;
   for i = 0; i < MAX; i++ {
      ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
   }
   for i = 0; i < MAX; i++ {
      fmt.Printf("a[%d]地址 = %d,a[%d]值 = %d,\n", i, ptr[i], i, *ptr[i])
   }
}

e8be461173cea8e1909072746ab21386.png
可以看到数组是引用类型,*号不在数组前面定义而是在其类型上定义。

陆 指针作为函数参数

Go 语言允许向函数传递指针,只需要在函数定义的参数上设置为指针类型即可。

以下实例演示了如何向函数传递指针,并在函数调用后修改函数内的值:

package main
import "fmt"
func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int= 200
   fmt.Printf("交换前 a 的值 : %d\n", a )
   fmt.Printf("交换前 b 的值 : %d\n", b )
   /* 调用函数用于交换值
   * &a 指向 a 变量的地址
   * &b 指向 b 变量的地址
   */
   swap(&a, &b);
   fmt.Printf("交换后 a 的值 : %d\n", a )
   fmt.Printf("交换后 b 的值 : %d\n", b )
}
func swap(x *int, y *int) {
   var temp int
   temp = *x    /* 保存 x 地址的值 */
   *x = *y      /* 将 y 赋值给 x */
   *y = temp    /* 将 temp 赋值给 y */
}

20eefe5dc5abab9542a34edcf223a7c1.png

柒 new和make

我们先来看一个例子:

func main() {
    var a *int
    *a = 100
    // 为什么可以b := 100,a = &b呢?
    // 个人理解是因为a=&b表示&b讲地址的值赋值给a,而*a=100,表示不能直接将100给*a,因为*a本身没有存储100的地址,需要申请一块存储100的地址,才能存储100
    fmt.Println(*a)

    var b map[string]int
    b["A7"] = 100
    fmt.Println(b)
}

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

7.1 new

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

func new(Type)	*Type
// Type:表示类型,new函数只接受一个参数,这个参数是一个类型
//*Type:表示类型指针,new函数返回一个指向该类型内存地址的指针。

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

func main() {
    a := new(int)
    b := new(bool)
    fmt.Printf("%T\n", a) // *int
    fmt.Printf("%T\n", b) // *bool
    fmt.Println(*a)       // 0
    fmt.Println(*b)       // false
}

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

func main() {
    var a *int
    a = new(int)
    *a = 10
    fmt.Println(*a)
}

7.2 new与make的区别

a. 二者都是用来做内存分配的
b. new用于的内存分配,会为类型的新值分配已置零的内存空间,返回的是指向类型的指针
c. 而make只用于slice、map以及channel的初始化,会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,说白了make返回初始化之后类型的值,这个值并不是这个类型的零值,也不是指针,是经过初始化之后的类型的引用,所以返回的还是这三个引用类型本身,new是代替不了make的

捌 什么情况下使用指针(注意)

  • a.不要对 map、slice、channel 这类引用类型使用指针;
  • b.如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
  • c.如果是比较大的数据类型时,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
  • d.像 int、bool 这样的小数据类型没必要使用指针;
  • e.如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;
  • f.指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使你的代码变得异常复杂。
avatar
小C&天天

修学储能 先博后渊


今日诗句