指针

指针

一 指针介绍

1.1 什么是指针

​ 我们将内存中字节单元的编号称为:地址(Address)

​ 如果把内存单元比喻为小柜子,那么内单元的地址就相当于柜子的编号,如下所示

​ 地址(Address)是用来帮我们找到存储在内存中的数据的,每个地址都指向一块存储空间,可以说“指向”二字很好地表达出了地址的精髓,为了能够更好地反映出地址的精髓,在高级语言中地址也被形象地称为:指针(Pointer)

1.2 如何获取指针

​ 指针就是地址,获取指针就是获取地址,获取变量的指针就是获取变量的地址(首地址)。

​ 对于一个变量

var x int = 100

​ 我们可以用&符拿到变量的地址,或者说拿到了一个指向变量的指针

fmt.Println(&x) // 0xc000016070

x=200 // 变量相当于一个容器,为变量填充一个新值,容器的地址不变
fmt.Println(&x) // 0xc000016070

1.3 通过指针可以操作变量值

​ 若想通过指针而非变量名去访问变量,需要在指针前加*号,如下

fmt.Println(*(&x)) // 100

​ 毫无疑问,我们通过指针与变量名操作的都是同一地址的数据

// 通过变量名修改值
x=200
fmt.Println(*(&x)) // 200
fmt.Println(x) // 200

// 通过变量的指针修改值
*(&x)=300
fmt.Println(*(&x)) // 300
fmt.Println(x) // 300 

1.4 指针的类型

​ 其实,指针是有类型的,

var x int = 100
var y string = "hello"

fmt.Printf("%T",&x) // 输出类型:*int
fmt.Printf("%T",&y) // 输出类型:*string

​ 但令人疑惑的是:指针就是地址,地址不就是一串十六进制表示的数字么(其实底层都是二进制),哪来的什么类型一说呢?

首先需知:变量名指向的变量地址是变量存储在内存中首个字节单元的地址,访问变量时,需要通过变量名指定的首地址,然后依据类型规定的大小,读取后续内容。

​然后:变量的指针与变量名一样,都是指向首地址的,所以指针也必须有类型,才能在取到首地址后根据类型的大小规定取出后续的内容

1.5 指针变量

​ 我们可以将指针/地址保存到一个变量中,这个变量称之为指针变量,指针变量里存放的是另外一个变量的地址

var x int = 100 
var p *int = &x 

// 有了指针变量p,*p就是*(&x),即*(变量x的地址),操作将会更加清晰
fmt.Println(p) // p的值为变量x的地址:0xc000016070
fmt.Println(*p) // *p即*(&x),结果为:100
fmt.Println(&p) // 变量p本身也肯定是有地址的:0xc00000e030

​ 一句话:指针就是地址, 指针变量就是存储地址的变量,变量名x与*p操作的都是同一个空间

x = 200
fmt.Println("x: ", x)   // 200
fmt.Println("*p: ", *p) // 200

//*p=300等同于x=300
*p = 300
fmt.Println("x: ", x)   // 300
fmt.Println("*p: ", *p) // 300

// ps:  
变量名x称之为直接引用
*p称之为间接引用或解引用:*p是通过借助变量x的地址来间接操作变量的空间的

二 指针的应用

2.1 储备知识:栈帧的内存布局

​ 程序的内存布局图如下

// 了解即可
代码区.text:存放CPU执行的机器指令,代码区是可共享,并且是只读的。

数据区.rodata与.data:存放已初始化的全局变量、静态变量(全局和局部)、常量数据。

.bss区:存放的是未初始化的全局变量和静态变量。

堆区:堆是由malloc()函数分配的内存块,向上增长。使用free()函数来释放内存,堆的申请释放工作由程序员控制,容易产生内存泄漏

栈区:首先在编译时由编译器自动分配释放好,然后在程序运行时,栈区由操作系统自动管理,无须程序员手动管理。存放函数的参数值、返回值和局部变量。,向下增长

我们的需要关注的是用户区,针对如下代码

package main

import "fmt"

func main() {
    var x int = 100
    var p *int = &x

    x = 200
    fmt.Println("x: ", x)   // 200
    fmt.Println("*p: ", *p) // 200

    //*p=300等同于x=300
    *p = 300
    fmt.Println("x: ", x)   // 300
    fmt.Println("*p: ", *p) // 300
}

上述代码都存储在用户区的stack(栈)区. 一般go程序调用 make() 或者 new()出来的都存储在用户区的heap(堆)区

接下来, 我们来了解一个新的概念: 栈帧.

栈帧: 用来给函数运行提供内存空间, 取内存于stack(栈)区上.

当函数调用时, 产生栈帧; 函数调用结束, 释放栈帧.

那么栈帧用来存放什么?

  • 局部变量
  • 形参
  • 内存字段描述值

其中, 形参与局部变量存储地位等同

当我们的程序运行时, 首先运行 main(), 这时就产生了一个栈帧.

当运行到 var x int = 100 时, 就会在栈帧里面产生一个空间.

同理, 运行到 var p *int = &x 时也会在栈帧里产生一个空间.

如下图所示

我们增加一个函数, 再来研究一下.

package main

import "fmt"

func foo(m int){
    var n int = 555
    n += m
}

func main() {
    var x int = 100
    var p *int = &x

    x = 200
    fmt.Println("x: ", x)   // 200
    fmt.Println("*p: ", *p) // 200

    //*p=300等同于x=300
    *p = 300
    fmt.Println("x: ", x)   // 300
    fmt.Println("*p: ", *p) // 300

    foo(111)
}

如下图所示, 当运行到 foo(111) 时, 会继续产生一个栈帧, 这时 main() 产生的栈帧还没有结束.

foo(111) 运行完毕时, 就会释放掉这个栈帧.

2.2 内置函数new(T)

野指针:初始值为无效的内存地址

var p *int = 0xc000016080 // 该内存地址不指向任何空间,无效

空指针:初始值为nil的指针

我们可以省略初始化表达式,使用零值初始化机制声明一个指针变量。注意:任何类型的指针的零值都是nil

var p *int // 注意*int不是int,*int的初始化零值为nil

fmt.Println(p) // <nil>

此时,p的初始值为

我们是无法进行如下赋值的

*p = 200 // 即*nil=200 ,*后跟的应该是一个地址,而nil不是一个地址,

我们当然可以在声明好指针变量p后,为其赋值一个地址,然后再进行上述赋值操作

// 先声明一个变量,绑定一个变名x
var x int = 100  

// 然后才能通过&x获取上述变量的地址,赋值给我们声明好的指针变量p
p = &x

// 可以正常执行下述赋值操作啦
*p = 200 // 即*(&x) = 100

但你们发现我们始终都绕不过一个变量名x,如果我们想不依赖于变量x,直接声明一个初始的零值不为nil的指针,那么指针变量的声明语句就不能省略初始化表达式了,必须使用内置函数new(T)作为初始化表达式

// 初始化表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T。
p:=new(int) // 此时指针p便不再是空指针或野指针

// 上述语句相当于执行了下面两步伪代码
var _ int // 先创建一个匿名变量
p:=&_    // 匿名变量不可用,此处为伪代码

​ 此时new()得到的指针变量p就可以赋值啦

*p = 200 // 改动的是匿名变量的值

2.3 变量的生命周期

联系管理员微信tutu19192010,注册账号

上一篇
下一篇
Copyright © 2022 Egon的技术星球 egonlin.com 版权所有 帮助IT小伙伴学到真正的技术