1.概念
指针是什么?
在
Go
语言中,如果我们要存储一个整数,我们会使用整型(int
),如果要存储一个字符串,我们会使用string
类型,如何我们想存储一个内存地址呢,要用什么数据类型呢?
答案是指针。
指针就是一个存储了其他数据项地址的数据项,或者说指针是一个存储了其他变量地址的变量。
在代码中,我们会经常存储或者读取各种数据,这些数据的数据类型可能是字符串、数字类型或结构体等,数据存在内存某个指定位置上,每个内存位置有自己的地址,指针就是专门用存储变量地址的变量,如下图所示:
从上面的示意图中可以看出一个指针类型的变量本身也有自己的内存地址。
2.指针类型
- 指针地址(&a)
- 指针取值(*&a)
- 指针类型(&a) —> *int 改变数据传指针
可以总结为:在编程语言中,指针是一种数据类型,用来存储一个内存地址,该地址指向存储在该内存中的对象。这个对象可以是字符串、整数、函数或者你自定义的结构体。
小技巧:你也可以简单地把指针理解为内存地址。
小提示:内存地址通常为 16 进制的数字表示,比如 0x45b876。
- 表示一个指向字符串类型的指针类型:
*string
- 表示一个指向整型64位的指针:
*int64
- 要指向一个复杂的数据结构,比如要存储一个如下所定义的结构体的指针:
type Student struct{ID stringName stringGrade string
}
指向该结构的指针类型如下:
*Student
3.认识指针地址和指针类型
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go 语言中使用&
作符放在变量前面对变量进行“取地址”操作。
格式如下:
ptr := &v // v的类型为T
其中 v 代表被取地址的变量,被取地址的 v 使用 ptr 变量进行接收,ptr 的类型就为*T
,称做 T 的指针类型。*
代表指针。
指针实际用法,通过下面的例子了解:
package main
import ("fmt"
)
func main() {var cat int = 1var str string = "banana"fmt.Printf("%p %p", &cat, &str)
}
运行结果:
0xc042052088 0xc0420461b0
从指针获取指针指向的值
package mainimport "fmt"func main() {var house ="malibu Point 10800 90265"//对字符串取地址,ptr类型为*stringptr := &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 // 定义包名为mainimport "fmt" // 导入fmt包,用于格式化输入输出// 定义一个Student结构体,包含ID,Name和Grade三个字段
type Student struct {ID stringName stringGrade string
}func main() { // 程序的入口函数var n int = 2 // 定义一个整型变量n并初始化为2p := &n // 定义一个整型指针p,并让它指向n*p = 10 // 通过指针p修改它指向的变量n的值为10fmt.Println(*p) // 打印p指向的值,即变量n的值,输出为10 // 修改了这里,使用*p来解引用指针// 创建一个指向Student结构体的指针,并初始化它的字段stu := &Student{ID: "001", Name: "test", Grade: "A"} fmt.Println(stu) // 打印stu指向的结构体的值,输出为Student的详细信息 // 修改了这里,直接打印结构体指针stu
}
&
与*
操作符的关系如下图所示:
5.指针的零值
任何指针变量的零值都是nil
:
package mainimport "fmt"type Student struct {ID stringName stringGrade string
}func main() {var p *intfmt.Println(p) //nilvar s *stringfmt.Println(s) //nilvar stu *Studentfmt.Println(stu) //nil
}
6.slice和指针
slice
也是引用类型,因此当把slice
作为参数传给函数时,对slice
变量的修改会生效:
package mainimport "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 mainimport "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 mainimport "fmt"type User struct {ID intName string
}func main() {var n = 10p := &nfmt.Println(n)*p = 20// p=20 是错误的fmt.Println(n)u := &User{ID: 1, Name: "A"}fmt.Println(u.Name)//Au.Name = "B"fmt.Println(u.Name) //B
}
上面示例中,可以看到,访问指向整型的指针变量时,需要在前面加上*
而访问指向结构体的指针变量则不需要加上*
上面的语句相当于:
u.Name = "B"
(*u).Name = "B"
8.方法的指针接收器
过方法对自身的值或属性,那么其接收器必须是指针类型的:
go 代码解读复制代码package mainimport "fmt"type Student struct {ID stringAge uint8Name 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)//小明
}
Rename
方法的接收器是*Student
类型,即Student
的指针。这意味着在Rename
方法内部,可以通过接收器指针直接修改Student
实例的属性。因此,在main
函数中调用s.Rename("小明")
后,s.Name
的值被成功修改为 "小明"。
Rename2
方法的接收器是Student
类型,即非指针类型。这意味着在Rename2
方法内部,接收器s
是Student
实例的一个副本,而不是原始实例的引用。在Rename2
方法内部对s
的任何修改都不会影响到原始实例。因此,在main
函数中调用s.Rename2("小华")
后,s.Name
的值仍然是 "小明",因为Rename2
方法修改的是s
的副本,而不是原始的Student
实例。