为什么需要指针?
简单地来说,就是保证软件功能实现的同时,尽量的节约内存资源,提高程序运行效率。
当给一个函数/方法传参的时候,我们要了解传进去的是值还是引用地址。当参数为基本类型时(如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 | var a int64 = 100 |
如图:
我们先定义一个变量a
,系统为变量a
分配了一个内存地址0x1234
,
通过 & 符可以将变量 a
的内存地址0x1234
取出来,并交给 ptr1
保存,于是ptr1
的值由空(nil
)变成了0x1234
,通过*ptr1
就可以展示刚才存入的内存地址了。
三、指向指针的指针
归根结底还是指针类型,但定义时多了个*
,例如:var ptr2 **int64
1 | var ptr2 **int64 |
如图:
接着前面的定义,我们定义了一个指向ptr1
的指针ptr2
,此时ptr2
的值为ptr1
的内存地址,通过&a, ptr1, *ptr2
都可以拿到内存地址0x1234
,通过a, *ptr1, **ptr2
都可以拿到值100
。
四、将指针作为函数参数
例如:我们用函数来实现交换a和b的值:
1 | var a,b int64 = 100,200 |
如图:
在swap
函数的形参中,我们定义了两个指针ptr1
、ptr2
分别用来接收a和b的地址,通过*ptr1
、*ptr2
分别拿到值进行交换。在Go语言中,交换值还有更简洁的操作:*ptr1,*ptr2 = *ptr2,*ptr1
,直接交换。
五、指针数组
说白了还是数组,只不过这个数组仅用来存放内存地址,也就有了一个书面术语——指针数组。
来看代码:
1 | var arr = [3]int64 {1,2,3} |
上面代码定义了一个指针数组ptrarr
,用于存放数组arr
中的每一个元素的内存地址,然后通过ptrarr
可以直接查看到值都是内存地址,通过*ptrarr[k]
就可以获取到arr
每一个元素的值。
有了指针,为什么还要有指针数组,因为数组(指针数组)在内存中的地址一般情况下都是连续分配的,比零散存放的变量(指针)查找存取更有效率,速度更快。
六、结构体指针
6.1 说白了还是指针,只不过这个指针是指向了一个结构体,
例如:
1 | type Person struct { |
从以上代码可以得出,定义结构体指针分四步:
1、定义结构体类型
2、定义指针
3、实例化结构体
4、指针指向结构体实例
与指向变量或数组的指针不一样的地方在于,多了一种读取方式:Go提供了一种隐式解引用特性,可以直接用
指针名.结构字段
的形式访问值,如上面代码:(*ptr1).name
或ptr1.name
都可以获取到值。
6.2 也可以将结构体指针作为函数参数
1 | func main() { |
总结
在学校学习C语言的时候,指针没学好,所以有些阴影,本文试着用简单的代码来理解GO的指针,希望给自己加深印象。不同于 C 语言,Golang 的指针是单独的类型,而不是 C 语言中的 int 类型,而且也不能对指针做整数运算。传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力。