指针使用入门与 unsafe.Pointer

指针简介

变量的本质对一块内存空间的命名,我们可以通过引用变量名来使用这块内容空间存储的值,而指针则是用来指向这些变量值所内存地址的值

注:变量值所在的内存地址的值不等于内存地址存储的变量值
注:如果一个变量是指针类型的,那么就可以用这个变量来存储指针类型的值

栗子:
package main

import "fmt"

func main() {
	a := 100
	var ptr *int  // 声明指针类型
	ptr = &a      // 初始化指针类型值为变量 a
	fmt.Println(ptr)
	fmt.Println(*ptr)
}

上面代码中ptr就是一个指针类型,表示指向存储int类型的指针。ptr本身是一个内存地址值,所以需要通过内存地址进行赋值(通过&a可以获取a所在的内存地址),赋值之后,可以通过*ptr获取指针指向内存地址存储的变量值(我们通常将这种引用称作[间接引语]),所以上述代码打印结果是:

0xc0000a2000
100

每次打印的prt值可能不一样,因为存储变量a的内存地址在变动,不同操作系统打印的结果也不相同。

Go语言之所以引入指针类型,主要基于两点考虑:一个是为程序员提供操作变量对应内存数据结构的能力;另一个是为了提高程序的性能(指针可以直接指向某个变量值的内存地址,可以记大节省内存空间,操作效率也更高),这在系统编程,操作系统或者网络应用中是不容忽视的因素

使用场景

指针在 Go 语言中有两个典型的使用场景:

  • 类型指针

  • 切片

作为类型指针时:允许对这个指针类型数据指向的内存地址存储值进行修改,传递数据时如果时用指针则无须拷贝数据从而节省内存空间,此外和C语言中的指针不同,Go语言中的类型指针不能进行偏移和运算,因此更为安全

切片类型: 由指向数组起始元素的指针、元素数量和容量组成,所以切片与数组不同,是引用类型,而非值类型

基本使用

指针类型的声明和初始化

指针变量在传值时之所以可以节省内存空间,是因为指针指向的内存地址的大小是固定的,在32位机器上占4个字节,在64位机器上占8个字节,这与指针指向内存地址存储的值类型无关。

指针类型变量也需要一块内存空间存储值,指针变量的值就是它所指向数据的内存地址,而普通变量的值就是具体存放的数据。不同的指针类型变量之间无法互相赋值,在 Golang 语言中,指针不支持运算,也不能获取常量的指针。

指针定义

在 Golang 语言中,指针定义有 3 种方式:

第一种方式使用取地址符&获取变量的指针(内存地址);

第二种方式使用var关键字声明指针变量,使用var关键字声明的变量不能直接赋值和取值,因为它还没有内存地址,它的值是nil;

第三种方式使用内置的new函数来声明指针类型的变量,new函数接收一个参数,可以传递类型给它,返回值是传递类型的指针类型。

栗子:

func main () {
 // 方式 1
 // 定义普通变量 a
 a := 1
 // 定义指针变量 p
 p := &a
 fmt.Println("变量 a 的值为:", a) // 1
 fmt.Println("变量 a 的内存地址为:", p) // 0xc0000ae008
 fmt.Printf("变量 a 的类型为:%T\n", a) // int
 fmt.Printf("变量 p 的类型为:%T\n", p) // *int
  
 // 方式 2
 var str string
 var p1 *int
 // 不同指针类型变量之间无法互相赋值
  p1 = &str // ./main.go:29:5: cannot use &str (type *string) as type *int in assignment

 // 方式 3
 p2 := new(int)
 fmt.Printf("%v %T\n", p2, p2)
}


指针操作

在 Golang 语言中,指针操作包括取值和修改。取值就是获取指针指向的值,只需在指针变量前加 *;修改就是修改指针指向的值,需要注意的是使用 var 关键字声明的指针变量不能直接赋值和取值,因为它还没有分配内存,它的值为 nil,可以使用内置函数 new 给它分配内存。

栗子:

func main () {
  // 获取指针指向的值
 b := 2
 p3 := &b
 val := *p3
 fmt.Println("变量 val 的值为:", val)

 // 修改指针指向的值
 // 给 *p3 赋值,*p3 指向的值也被修改,因为 p3 指向的内存就是变量 b 的内存地址。
 *p3 = 3
 fmt.Println("*p3 指针指向的值为:", *p3)
 fmt.Println("变量 b 的值为:", b)
  
 var p4 *int = new(int)
 *p4 = 4
 fmt.Println(*p4)
}
指针应用

指针参数:

在 Golang 语言中,函数传递参数只有值传递,传递的实参都是参数原始值的拷贝副本,所以我们传递值类型的参数时,修改参数的值,原始数据不会被修改。但是,如果是指针类型的参数,修改参数的值,原始数据也会被修改,原因是指针类型的参数存储的是内存地址,并且和实参的内存地址相同

栗子:

func main () {
  // 值类型参数,实参的值未改变
 mySalary := 80000
 fmt.Printf("变量 mySalary 的内存地址为:%p\n", &mySalary)
 modifySalary(mySalary)
 fmt.Println(mySalary)

 // 指针类型参数,实参的值被改变
 modifySalary2(&mySalary)
 fmt.Println(mySalary)
}

func modifySalary (salary int) {
 fmt.Printf("参数变量的内存地址为:%p\n", &salary)
 salary = 100000
}

func modifySalary2 (salary *int) {
 fmt.Printf("参数变量的内存地址为:%p\n", salary)
 *salary = 100000
}

指针接收者:

在 Golang 语言中,定义一个方法,接收者可以是值类型和指针类型,二者都可以调用方法,因为 Golang 编译器会自动转换,所以二者是等价的。

type worker struct {
 name string
 salary uint
}

func (w *worker) raise () {
 w.salary += 1000
}

func (w worker) raise1 () {
 w.salary += 1000
}

func main () {
  // 值类型调用者
 w := worker{
  name: "frank",
  salary: 5000,
 }
 // 指针类型接收者
 w.raise()
 fmt.Printf("w 的姓名是 %s,薪水是每月 %d\n", w.name, w.salary)

 // 值类型调用者
 w1 :=worker{
  name: "frank1",
  salary: 5000,
 }
 // 值类型接收者
 w1.raise1()
 fmt.Printf("w1 的姓名是 %s,薪水是每月 %d\n", w1.name, w1.salary)

 // 指针类型调用者
 w2 := &worker{
  name: "lucy",
  salary: 5000,
 }
 // 指针类型接收者
 w2.raise()
 fmt.Printf("w2 的姓名是 %s,薪水是每月 %d\n", w2.name, w2.salary)

 // 指针类型调用者
 w3 := &worker{
  name: "lucy1",
  salary: 5000,
 }
 // 值类型接收者
 w3.raise1()
 fmt.Printf("w3 的姓名是 %s,薪水是每月 %d\n", w3.name, w3.salary)
}

那么,应该在什么时候使用指针接收者呢?

我总结了以下两点:

  • 如果需要修改接收者,可以使用指针修改指针指向数据的值。
  • 如果接收者是非 map、slice 和 channel 类型,并且数据比较大,可以使用指针来节省内存。

unsafe.Pointer

我们前面介绍的指针都是被声明为指定类型的,而 unsafe.Pointer 是特别定义的一种指针类型,它可以包含任意类型变量的地址(类似 C 语言中的 void 类型指针)。Go 官方文档对这个类型有如下四个描述:

  • 任何类型的指针都可以被转化为 unsafe.Pointer;

  • unsafe.Pointer 可以被转化为任何类型的指针;

  • uintptr 可以被转化为 unsafe.Pointer;

  • unsafe.Pointer 可以被转化为 uintptr。

指针类型转化

因此,unsafe.Pointer 可以在不同的指针类型之间做转化,从而可以表示任意可寻址的指针类型:

package main

import (
	"fmt"
	"unsafe"
)

func swap(a, b *int)  {
	*a, *b = *b, *a
	fmt.Println("swap",*a, *b)
}

func main() {
	i := 10
	var p *int = &i

	var fp *float32 = (*float32)(unsafe.Pointer(p))
	*fp = *fp * 10
	fmt.Println(i)  // 100
}

这里,我们将指向 int 类型的指针转化为了 unsafe.Pointer 类型,再转化为 *float32 类型(参考前面的 unsafe.Pointer 转化规则 1、2)并进行运算,最后发现 i 的值发生了改变。

这个示例说明了 unsafe.Pointer 是一个万能指针,可以在任何指针类型之间做转化,这就绕过了 Go 的类型安全机制,所以是不安全的操作。

指针运算实现

此外,根据上面的转化规则 3、4,unsafe.Pointer 还可以与 uintptr 类型之间相互转化,为什么要单独列出这个类型呢?

uintptr 是 Go 内置的可用于存储指针的整型,而整型是可以进行数学运算的!因此,将 unsafe.Pointer 转化为 uintptr 类型后,就可以让本不具备运算能力的指针具备了指针运算能力:

这里,我们将数组 arr 的内存地址赋值给指针 ap,然后通过 unsafe.Pointer 这个桥梁转化为 uintptr 类型,再加上数组元素偏移量(通过 unsafe.Sizeof 函数获取),就可以得到该数组第二个元素的内存地址,最后通过 unsafe.Pointer 将其转化为 int 类型指针赋值给 sp 指针,并进行修改,最终打印的结果是:

[1 5 3]

这样一来,就可以绕过 Go 指针的安全限制,实现对指针的动态偏移和计算了,这会导致即使发生数组越界了,也不会报错,而是返回下一个内存地址存储的值,这就破坏了内存安全限制,所以这也是不安全的操作,我们在实际编码时要尽量避免使用,必须使用的话也要非常谨慎。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值