15.Go复合类型-指针
7:指针
7.1 变量内存与地址
前面我们讲过存储数据的方式,可以通过变量,或者复合类型中的数组,切片,Map,结构体。
我们不管使用变量存储数据,还是使用符合类型存储数据,都有两层的含义:
- 存储的数据(内存),对应的地址。
接下来,通过变量来说明以上两个含义。
例如,定义如下变量,打印变量的值以及存储的内存地址:
var i int
i = 100
fmt.Printf("i=%d\n", i)
fmt.Printf("&i=%v\n", &i)
// 执行如下
i=100
&i=0xc00000a0a8
- 第一个
Printf()
函数的输出,大家都很熟悉,输出变量i
的值,这个实际上就是输出内存中存储的数据。在前面的章节中,已经讲解过,定义一个变量,就是在内存中开辟一个空间,用来存储数据,当给变量i
赋值为100,其实就是将100存储在该空间内。 - 第二个
Printf()
函数的输出,输出的是变量i
在内存中的地址。通过如下图来给大家解释:
img
这张图,大家也应该非常熟悉,是在讲解变量时,画的一张图,0x100010
假设是变量i
的内存地址(通过第二个输出可以获取实际的地址),内存地址的作用:在输出变量中存储的数据时,是通过地址来找到该变量内存空间的。
这个内存地址和实际生活中的地址也很相似,例如:大家可以将内存空间想象成,我们上课的教室,教室中存放有学生,那么现在要找一个学生,必须要知道具体的地址以及教室门牌号。
7.2 指针变量
现在已经知道怎样获取变量在内存中的地址,但是如果想将获取的地址进行保存,应该怎样做呢?
可以通过指针变量来存储,所谓的指针变量:就是用来存储任何一个值的内存地址。
指针变量的定义如下:
// 指针变量的定义
var i int = 100
var p *int // 定义指针变量
p = &i // 把变量i的地址赋值给指针变量p
fmt.Printf("i=%d, p=%v", i, p)
// 执行如下:
i=100, p=0xc00000a0a8
指针变量p
的定义是通过*
这个符号来定义,指针变量p
的类型为*int
, 表示存储的是一个整型变量的地址。
如果指针变量p
存储的是一个字符串类型变量的地址,那么指针变量p
的类型为*string
p=&i
: 该行代码的意思是,将变量i
的地址取出来,并且赋值给指针变量p
.也就是指针变量p
指向了变量i
的存储单元。
可以通过如下图来表示:
image-20210529105440322
在以上图中,一定要注意:指针变量p
存储的是变量i
的地址。
大家可以思考一个问题:使用指针修改变量的值
既然指针变量p
指向了变量i
的存储单元,那么是否可以通过指针变量p
,来操作变量i
中存储的数据?
答案是可以的,具体操作方式如下:
// 指针变量的定义
var i int = 100
var p *int // 定义指针变量
p = &i // 把变量i的地址赋值给指针变量p
*p = 80 // 使用指针修改值 《=====
fmt.Printf("i=%d, p=%v", i, p)
// 执行
i=80, p=0xc00000a0a8
注意:在使用指针变量p
来修改变量i
的值的时候,前面一定要加上*
.(通过指针访问目标对象)
现在打印变量i
的值已经有100变为80.
使用指针变量输出变量的值
当然,也可以通过指针变量p
来输出,变量i
中的值,输出的方式如下所示:
// 通过指针变量`p`来输出,变量`i`中的值
fmt.Printf("i=%d, *p=%v", i, *p)
所以,*p
的作用就是根据存储的变量的地址,来操作变量的存储单元(包括输出变量存储单元中的值,和对值进行修改)
7.3 注意事项
在使用指针变量时,要注意以下两点。
1:默认值为nil
var p *int
fmt.Println(p)
// 执行
<nil>
直接执行上面的程序,结果是:nil
2: 不要操作没有合法指向的内存。
例如,在上面的案例中,我们定义了指针变量p,但是没有让指针变量指向任何一个变量,那么直接运行如下程序,会出现异常。
var p *int
*p = 56 // 没有指向内存空间,直接赋值
fmt.Println(p)
出现的错误信息如下:
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x1 addr=0x0 pc=0x14579a6]
所以,在使用指针变量时,一定要让指针变量有正确的指向。
以下的操作是合法的:
var a int
var p *int
p = &a // 指向变量a的内存地址
*p = 56
fmt.Println(a)
在该案例中,定义了一个变量a, 同时定义了一个指针变量p, 将变量a的地址赋值给指针变量p,也就是指针变量p指向了变量a的存储单元。给指针变量p赋值,影响到了变量a.最终输出变量a中的值也是56.
7.4 new( )函数:指针变量指向新的内存空间地址
指针变量,除了以上介绍的指向以外(p=&a
),还可以通过new( )
函数来指向。
具体的应用方式如下:
// new函数
var p *int
p = new(int)
*p = 57
fmt.Println(*p)
new(int)
作用就是创建一个整型大小(4字节)的空间
然后让指针变量p指向了该空间,所以通过指针变量p
进行赋值后,该空间中的值就是57
.
new()
函数的作用就是C语言中的动态分配空间。
但是在这里与C语言不同的地方,就是最后不需要关心该空间的释放。
GO语言会自动释放。这也是比C语言使用方便的地方。
也可以使用自动推导类型的方式:
q := new(int)
*q = 787
fmt.Println(*q)
7.5 指针做函数参数
指针也可以作为函数参数,那么指针作为函数参数在进行传递的时候,是值传递还是引用传递呢?
大家都知道,普通变量作为函数参数进行传递是值传递,如下案例所示:
定义一个函数,实现两个变量值的交换。
func Swap(num1, num2 int) {
num1, num2 = num2, num1
fmt.Printf("num1 = %d, num2 = %d\n", num1, num2)
}
func main() {
var a int = 10
var b int = 20
Swap(a, b)
fmt.Printf("a = %d, b = %d", a, b)
}
//执行如下:
num1 = 20, num2 = 10
a = 10, b = 20
通过以上案例,证实普通类型变量在传递时,为值传递。
使用指针作为函数参数,进行值交换
那么使用指针作为函数参数呢?现在将以上案例修改成,用指针作为参数,如下所示:
func Swap(num1, num2 *int) {
*num1, *num2 = *num2, *num1
fmt.Printf("num1 = %d, num2 = %d\n", *num1, *num2)
}
func main() {
var a int = 10
var b int = 20
Swap(&a, &b)
fmt.Printf("a = %d, b = %d", a, b)
}
//执行:
num1 = 20, num2 = 10
a = 20, b = 10
通过以上案例证实,指针作为参数进行传递时,为引用传递,也就是传递的地址。
在调用Swap()
函数时,将变量a与变量b的地址传分别传递给指针变量num1,num2,这时num1和num2,分别指向了变量a,与变量b的内存存储单元,那么操作num1,num2实际上操作的就是变量a与变量b,所以变量a与变量b的值被交换。
7.6 数组指针
前面在讲解数组的时候,我们用数组作为函数参数,但是数组作为参数进行传递是值传递,如果想引用传递,可以使用数组指针。
具体使用方式如下:
func Swap(p *[5]int) { // 设置指针数组为参数
(*p)[0] = 89
}
func main() {
a := [5]int{1, 2, 3, 4, 5}
Swap(&a) // 传递数组地址
fmt.Println(a)
}
// 执行:
[89 2 3 4 5]
定义一个数组,作为函数Swap的实参进行传递,但是这里传递的是数组的地址,所以Swap的形参是数组指针。
这时指针p
,指向了数组a
,对指针p
的操作实际上是对数组a
的操作,所以如果直接执行如下语句:fmt.Println(*p)
,会输出数组a
中的值。
也可以通过*p
结合下标将对应的值取出来进行修改。
最终在main
函数中输出数组a
,发现其元素也已经修改。
当然,我们也可以通过循环的方式来将数组指针中的数据打印出来:
for index, value := range *p{
fmt.Printf("index=%d, value=%d\n", index, value)
}
7.7 指针数组
上一小节,讲解到的是数组指针,也就是让一个指针指向数组 ,然后可以通过该指针来操作数组。
还有一个概念叫指针数组,这两个概念很容混淆,指针数组指的是一个数组中存储的都是指针(也就是地址)。
也就是一个存储了地址的数组。
指针数组的定义
下面通过一个案例,看一下指针数组的应用
// 指针数组的定义
var p [2]*int // 指针数组的定义
var i int = 10
var j int = 20
// 将地址赋值给指针数组
p[0] = &i
p[1] = &j
fmt.Println(p[0])
fmt.Println(p[1])
// 执行如下:
0xc00000a0a8
0xc00000a0c0
指针数组的定义方式,与数组指针定义方式是不一样的,注意指针数组是将“*”
放在了下标的后面 var p [2]*int
。
由于指针数组存储的都是地址,所以将变量i
,与变量j
的地址赋值给了指针数组p。
最后输出指针数组p中存储的地址。
指针数组操作指向变量的值
思考:既然指针数组p
存储了变量i
和变量j
的的地址,那么怎样通过指针数组p
操作变量i
与变量j
的值呢?
具体实现如下:
// 指针数组的定义
var p [2]*int // 指针数组的定义
var i int = 10
var j int = 20
// 将地址赋值给指针数组
p[0] = &i
p[1] = &j
fmt.Println(*p[0])
fmt.Println(*p[1])
// 执行:
10
20
注意这里输出 fmt.Println(*p[0])
要注意的问题是,没有加小括号。(注意运算顺序)
当然,我们也可以通过for循环的方式来输出指针数组中对应的值。
// 将地址赋值给指针数组
p[0] = &i
p[1] = &j
// for循环输出指针数组中对应的值
for k, v := range p {
fmt.Printf("k=%d, v=%d\n", k, *v)
}
// 执行:
k=0, v=10
k=1, v=20
7.8 结构体指针变量
我们前面定义了指针指向了数组,解决了数组引用传递的问题。那么指针是否可以指向结构体,也能够解决结构体引用传递的问题呢?完全可以。
结构体指针变量的定义:
下面我们先来看一下,结构体指针变量的定义:
type Student struct {
id int
name string
score float64
}
func main() {
var p *Student = &Student{1,"zhangsan", 90} // 定义结构体指针
fmt.Println(*p)
}
// 执行如下:
{1 zhangsan 90}
自动推导类型定义结构体指针变量:
也可以使用自动推导类型
type Student struct {
id int
name string
score float64
}
func main() {
p := &Student{1,"zhangsan", 90} // 自动推导,定义结构体指针
fmt.Println(*p)
}
使用结构体指针操作结构体成员属性
现在定义了一个结构体指针变量,那么可以通过该指针变量来操作结构体中的成员项。
type Student struct {
id int
name string
score float64
}
func main() {
p := &Student{1,"zhangsan", 90} // 自动推导,定义结构体指针
p.score = 89 // 直接通过 . 运算符操作结构体属性
fmt.Println(*p)
}
// 执行如下:
{1 zhangsan 89}
结构体指针作为形参
前面在讲解结构体时,用结构体作为函数的参数,默认的是值传递。
那么通过结构体指针,可以实现结构体的引用传递。
具体实现的方式如下:
type Student struct {
id int
name string
score float64
}
// 结构体指针变量作为形参
func Test(p *Student) {
p.id = 19
}
func main() {
p := &Student{1, "zhangsan", 90} // 自动推导,定义结构体指针
p.score = 89 // 直接通过 . 运算符操作结构体属性
Test(p) // 传递结构体指针
fmt.Println(*p)
}
// 执行如下:
{19 zhangsan 89}