0%

理解GO指针

为什么需要指针?

简单地来说,就是保证软件功能实现的同时,尽量的节约内存资源,提高程序运行效率。

当给一个函数/方法传参的时候,我们要了解传进去的是值还是引用地址。当参数为基本类型时(如string, bool, int 及 float),传进去的大都是值,也就是另外复制了一份参数到当前的函数调用栈;当参数为高级类型时(如struct,array/slice,map,chan,func),传进去的基本都是引用地址,这个主要是因为虚拟机的内存管理导致的。

内存管理中的内存区域一般包括 heap(堆)和 stack(栈), stack 主要用来存储当前调用栈用到的简单类型数据:string,bool,int,float 等,这些类型的内存占用小,容易回收,基本上它们的值和指针占用的空间差不多,因此可以直接复制,GC也比较容易做针对性的优化。 复杂的高级类型占用的内存往往相对较大,存储在 heap 中,GC 回收频率相对较低,代价也较大,因此传引用/指针可以避免进行成本较高的复制操作,并且节省内存,提高程序运行效率。
关于堆和栈不太理解的童鞋,可以阅读:堆和栈的区别

因此,在下列情况可以考虑使用指针:
1、需要改变参数的值;
2、避免复制操作;
3、节省内存。

一、定义指针

var ptr1 *int64
刚定义的指针没有指向任何地址,为nil,所以也叫空指针。
如图:

我们假设图片上表示的是一个内存块,存放有变量名(户口)和值(住几口人),以及它的内存地址(门牌号),指针可以理解成一个特殊的变量,它也需要占用内存,它的特殊之处在于,它的值必须且只能存放内存地址,假设它在内存中的地址为:0xabcd

二、使用指针

1
2
3
var a int64 = 100
ptr1 = &a
fmt.Println(*ptr1)

如图:

我们先定义一个变量a,系统为变量a分配了一个内存地址0x1234
通过 & 符可以将变量 a 的内存地址0x1234取出来,并交给 ptr1保存,于是ptr1的值由空(nil)变成了0x1234,通过*ptr1就可以展示刚才存入的内存地址了。

三、指向指针的指针

归根结底还是指针类型,但定义时多了个*,例如:var ptr2 **int64

1
2
3
4
var ptr2 **int64
ptr2 = &ptr1 //ptr2为指针向指针的指针
fmt.Println(&a, ptr1, *ptr2) //三者完全等价
fmt.Println(a, *ptr1, **ptr2) //三者完全等价

如图:
接着前面的定义,我们定义了一个指向ptr1的指针ptr2,此时ptr2的值为ptr1的内存地址,通过&a, ptr1, *ptr2都可以拿到内存地址0x1234,通过a, *ptr1, **ptr2都可以拿到值100

四、将指针作为函数参数

例如:我们用函数来实现交换a和b的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a,b int64 = 100,200
fmt.Printf("交换前,a=%d,b=%d\n", a, b)
swap(&a, &b)
fmt.Printf("交换后,a=%d,b=%d\n", a, b)

//通过传递引用,交换两个变量的值
func swap(ptr1 *int64, ptr2 *int64) {
var tmp int64
tmp = *ptr1
*ptr1 = *ptr2
*ptr2 = tmp
//*ptr1,*ptr2 = *ptr2,*ptr1 //交换语句这样写更加简洁,也是 go 语言的特性,c++ 和 c# 是不能这么干的
}

如图:

swap函数的形参中,我们定义了两个指针ptr1ptr2分别用来接收a和b的地址,通过*ptr1*ptr2分别拿到值进行交换。在Go语言中,交换值还有更简洁的操作:*ptr1,*ptr2 = *ptr2,*ptr1,直接交换。

五、指针数组

说白了还是数组,只不过这个数组仅用来存放内存地址,也就有了一个书面术语——指针数组。
来看代码:

1
2
3
4
5
6
7
8
9
10
var arr = [3]int64 {1,2,3}
var ptrarr [3]*int64
for k,_ := range arr {
ptrarr[k] = &arr[k]
}
fmt.Println("指针指向的内存地址有:", ptrarr)
fmt.Println("遍历指针数组的值:")
for k,_ := range ptrarr {
fmt.Println(*ptrarr[k]);
}

上面代码定义了一个指针数组ptrarr,用于存放数组arr中的每一个元素的内存地址,然后通过ptrarr可以直接查看到值都是内存地址,通过*ptrarr[k]就可以获取到arr每一个元素的值。

有了指针,为什么还要有指针数组,因为数组(指针数组)在内存中的地址一般情况下都是连续分配的,比零散存放的变量(指针)查找存取更有效率,速度更快。

六、结构体指针

6.1 说白了还是指针,只不过这个指针是指向了一个结构体,
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Person struct {
name string
sex rune //rune等同于int32
height uint8
}

var ouyang Person
ouyang.name = "欧阳"
ouyang.sex = '男'
ouyang.height = 175
//也可以这样赋值:ouyang := Person("欧阳", '男', 175)

var ptr1 *Person //定义结构体指针,格式:var 指针名称 *结构体名
ptr1 = &ouyang
fmt.Println((*ptr1).name, ptr1.name)

从以上代码可以得出,定义结构体指针分四步:
1、定义结构体类型
2、定义指针
3、实例化结构体
4、指针指向结构体实例

与指向变量或数组的指针不一样的地方在于,多了一种读取方式:Go提供了一种隐式解引用特性,可以直接用指针名.结构字段的形式访问值,如上面代码:(*ptr1).nameptr1.name都可以获取到值。

6.2 也可以将结构体指针作为函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main()  {
type Person struct {
name string
sex rune //rune等同于int32
height uint8
}
var ouyang Person
ouyang.name = "欧阳"
ouyang.sex = '男'
ouyang.height = 175
printPerson(&ouyang)
}

func printPerson(ptr1 *Person) {
fmt.Println(ptr1.name, ptr1.sex, ptr1.height)
}

总结

在学校学习C语言的时候,指针没学好,所以有些阴影,本文试着用简单的代码来理解GO的指针,希望给自己加深印象。不同于 C 语言,Golang 的指针是单独的类型,而不是 C 语言中的 int 类型,而且也不能对指针做整数运算。传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力。