当前位置: 首页 > article >正文

go语言中的指针详解

1.概念

指针是什么?

Go语言中,如果我们要存储一个整数,我们会使用整型(int),如果要存储一个字符串,我们会使用string类型,如何我们想存储一个内存地址呢,要用什么数据类型呢?

答案是指针

指针就是一个存储了其他数据项地址的数据项,或者说指针是一个存储了其他变量地址的变量。

在代码中,我们会经常存储或者读取各种数据,这些数据的数据类型可能是字符串、数字类型或结构体等,数据存在内存某个指定位置上,每个内存位置有自己的地址,指针就是专门用存储变量地址的变量,如下图所示:

从上面的示意图中可以看出一个指针类型的变量本身也有自己的内存地址。 

2.指针类型

  • 指针地址(&a)
  • 指针取值(*&a)
  • 指针类型(&a) —> *int 改变数据传指针

可以总结为:在编程语言中,指针是一种数据类型,用来存储一个内存地址该地址指向存储在该内存中的对象。这个对象可以是字符串、整数、函数或者你自定义的结构体。 

小技巧:你也可以简单地把指针理解为内存地址。 

小提示:内存地址通常为 16 进制的数字表示,比如 0x45b876。  

  • 表示一个指向字符串类型的指针类型:
*string
  • 表示一个指向整型64位的指针:
*int64
  • 要指向一个复杂的数据结构,比如要存储一个如下所定义的结构体的指针:
type Student struct{
	ID    string
	Name  string
	Grade string
}

指向该结构的指针类型如下:

*Student

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

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

格式如下:

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

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

指针实际用法,通过下面的例子了解:

package main
import (
    "fmt"
)
func main() {
    var cat int = 1
    var str string = "banana"
    fmt.Printf("%p %p", &cat, &str)
}
运行结果:
0xc042052088 0xc0420461b0

 从指针获取指针指向的值

package main

import "fmt"

func main() {
   var house ="malibu Point 10800 90265"
	 //对字符串取地址,ptr类型为*string
	 ptr := &house
	 //打印ptr的类型
	 fmt.Printf("ptr type :%T\n",ptr)
	 //打印ptr的指针地址
	 fmt.Printf("address :%p\n",ptr)
	 //怼指针进行取值操作
	 value := *ptr
	 //取值后的类型
	 fmt.Printf("value type:%T\n",value)
	 //指针取值后就是指向变量的值
	 fmt.Printf("value :%s\n",value)

}

运行结果:

ptr type :*string
address :0xc0000200a0
value type:string
value :malibu Point 10800 90265

4.指针类型变量

package main  // 定义包名为main

import "fmt"  // 导入fmt包,用于格式化输入输出

// 定义一个Student结构体,包含ID,Name和Grade三个字段
type Student struct {
    ID    string
    Name  string
    Grade string
}

func main() {  // 程序的入口函数
    var n int = 2  // 定义一个整型变量n并初始化为2
    p := &n  // 定义一个整型指针p,并让它指向n
    *p = 10  // 通过指针p修改它指向的变量n的值为10
    fmt.Println(*p) // 打印p指向的值,即变量n的值,输出为10 // 修改了这里,使用*p来解引用指针

    // 创建一个指向Student结构体的指针,并初始化它的字段
    stu := &Student{ID: "001", Name: "test", Grade: "A"} 
    fmt.Println(stu) // 打印stu指向的结构体的值,输出为Student的详细信息 // 修改了这里,直接打印结构体指针stu
}

 &*操作符的关系如下图所示:

5.指针的零值 

任何指针变量的零值都是nil:

package main

import "fmt"

type Student struct {
	ID    string
	Name  string
	Grade string
}

func main() {
	var p *int
	fmt.Println(p) //nil

	var s *string
	fmt.Println(s) //nil

	var stu *Student
	fmt.Println(stu) //nil
}

6.slice和指针

slice也是引用类型,因此当把slice作为参数传给函数时,对slice变量的修改会生效:

package main

import "fmt"

func ChangeFirstItem(lgB []string) {
	lgB[0] = "C"
}

func main() {
	lgA := []string{"C++", "JavaScript", "Python", "PHP"}
	fmt.Println("修改前:", lgA)
	ChangeFirstItem(lgA)
	fmt.Println("修改后:", lgA)
}

运行结果:

修改前: [C++ JavaScript Python PHP]
修改后: [C JavaScript Python PHP]

可以看出,在函数内对slice类型变量的修改生效了。

接下来的对同一个slice,我们往slice里添加元素:

package main

import "fmt"

func AddItem(lgB []string) {
	lgB = append(lgB, "Rust")
	fmt.Println("Add函数内:", lgB)
}

func main() {
	lgA := []string{"C++", "JavaScript", "Python", "PHP"}
	fmt.Println("添加前:", lgA)
	AddItem(lgA)
	fmt.Println("添加后:", lgA)
}

我们看到,上面程序运行过程中,slice变量lgB在函数AddItem被修改了,但外面的slice变量lgA却没有变化。

为什么同样把slice变量作为函数的参数,ChangeFirstItem函数可以对slice变量lgB修改后,lgA也被修改了,而AddItem函数就不可以呢?

其实,当我们把一个slice变量lgA作为实参传给函数的形参时,实参与形参就是两个不同的slice变量(发生了复制),只不过这两个slice变量引用了同一个底层数组,如下图所示:

调用ChangeFirstItem函数只是修改了slice的第一个元素,也就是修改了底层数组的第一个元素,函数执行后,两个slice变量仍然是引用同一个底层数组。

而调用AddItem函数时,此时会向底层数组的尾部插入一个元素,但由于底层数组已没有容量了,Go会复制一个新的底层数组,把容量扩充一倍,因此执行AddItem函数后,AddItem的形参指向的是一个新的底层数组,而实参仍然指向旧的底层数组,如下图所示:

 7.struct与指针

指向结构体的指针变量,不需要在前面星号*就可以直接访问指向的结构体: 

package main

import "fmt"

type User struct {
	ID   int
	Name string
}

func main() {

	var n = 10
	p := &n
	fmt.Println(n)
	*p = 20
  // p=20 是错误的
	fmt.Println(n)

	u := &User{ID: 1, Name: "A"}

	fmt.Println(u.Name)//A
	u.Name = "B"
	fmt.Println(u.Name) //B 
}

上面示例中,可以看到,访问指向整型的指针变量时,需要在前面加上*

而访问指向结构体的指针变量则不需要加上*

上面的语句相当于:

u.Name = "B"
(*u).Name = "B"

8.方法的指针接收器 

过方法对自身的值或属性,那么其接收器必须是指针类型的:
go 代码解读复制代码package main

import "fmt"

type Student struct {
	ID   string
	Age  uint8
	Name string
}

func (s *Student) Rename(newName string) {
	s.Name = newName
}

func (s Student) Rename2(newName string) {
	s.Name = newName
}

func main() {
	s := Student{ID: "001", Age: 18, Name: "小张"}
	s.Rename("小明")
	fmt.Println(s.Name) //小明

	s.Rename2("小华")
	fmt.Println(s.Name)//小明
}

  1. Rename 方法的接收器是 *Student 类型,即 Student 的指针。这意味着在 Rename 方法内部,可以通过接收器指针直接修改 Student 实例的属性。因此,在 main 函数中调用 s.Rename("小明") 后,s.Name 的值被成功修改为 "小明"。

  2. Rename2 方法的接收器是 Student 类型,即非指针类型。这意味着在 Rename2 方法内部,接收器 sStudent 实例的一个副本,而不是原始实例的引用。在 Rename2 方法内部对 s 的任何修改都不会影响到原始实例。因此,在 main 函数中调用 s.Rename2("小华") 后,s.Name 的值仍然是 "小明",因为 Rename2 方法修改的是 s 的副本,而不是原始的 Student 实例。


http://www.kler.cn/a/407384.html

相关文章:

  • 力扣第 61 题旋转链表
  • LeetCode题解:28.找出字符串中第一个匹配项的下标【Python题解超详细,滑动窗口法、内置 find 函数、KMP算法】,知识拓展, KMP算法
  • Java与Kotlin在鸿蒙中的地位
  • HAProxy面试题及参考答案(精选80道面试题)
  • 文件管理 IV(文件系统)
  • 快速入门消息队列MQ、RabbitMQ
  • 19.QT程序简单的运行脚本
  • 【最优清零方案——贪心+滑动窗口+线段树】
  • TIM输入捕获
  • 网页F12:缓存的使用(设值、取值、删除)
  • 远程控制软件使用教程
  • 使用bcc/memleak定位C/C++应用的内存泄露问题
  • uni-app打包H5自定义微信分享
  • 【eNSP】动态路由协议RIP和OSPF
  • [JLOI2014] 松鼠的新家(重链剖分+线段树)
  • Python完成达梦数据库备注到MySQL数据备注的脚本
  • 探索 .NET 9 控制台应用中的 LiteDB 异步 CRUD 操作
  • 141. Sprite标签(Canvas作为贴图)
  • 大数据新视界 -- 大数据大厂之 Impala 性能优化:融合人工智能预测的资源预分配秘籍(上)(29 / 30)
  • 电子应用设计方案-19:智能云饭锅系统方案设计