golang中的指针

golang中的指针

1. 变量、地址与指针

在程序中,变量是一种占位符,用于指代内存中某一段的值。每一个变量都对应一个内存地址,在该地址上存储着该变量的内容。

我们常说的“指针”也是一种特殊的变量,特殊之处在于,指针变量存储的是内存中的某处地址,也就是说可以根据指针变量的内容到对应的变量单元,由此我们将这种存储内存地址的变量称作“指针”。

2. go语言中的符号*与&

在go中,符号&用于取地址,例如:

package main

import "fmt"

func main() {
	var a int = 10
	fmt.Printf("a的存储地址为: %p\n", &a)
    //a的存储地址为: 0xc0000121c0
}

输出为:

a的存储地址为: 0xc0000121c0

符号*有两种作用:

(1)声明变量时放在类型前,表明变量为指针变量,如

var i *int //声明i为指向整型的指针变量

(2)放在指针变量前,用于取出该指针所指向的值,如

func main() {
	var a int = 8
    var ptr *int = &a
    fmt.Printf("ptr指向的地址为: %v\n", ptr)
    fmt.Printf("ptr指向的地址中存储的内容为: %v\n", *ptr) //此处*用于从地址中取值
}

输出为:

ptr指向的地址为: 0xc0000121c0
ptr指向的地址所存储的内容为: 8

3. 指针的指针

文章开头提到,指针自身也是一种特殊的变量,意味着指针也会有自己的存储地址,故若一个指针变量指向另一个指针的地址,我们便称其为指向指针的指针变量,采用如下方式声明:

var pptr **int

声明指针的指针需要两个*号,类似的,我们还可以用三个*号来声明指向指向指针的指针的指针变量....当然,这在实际使用中绝少用到。事实上,指针的指针的实际使用频率也不会很高,这里提及此概念,主要是为了理解指针也是一种变量

给出如下一段指针的指针使用示例

func main() {
	var a int = 1
	var ptr *int = &a
	var ptrToPtr **int = &ptr
	//注意:此处不能写&&a,因为&a的结果没有存入指定的变量,无法对其取地址
	fmt.Printf("a = %d\n", a)
	fmt.Printf("指针ptr指向地址 %p\n", ptr)
	fmt.Printf("指针的指针ptrToPtr指向地址 %p\n", ptrToPtr)
	fmt.Printf("指针 *ptr = %d\n", *ptr)
	fmt.Printf("指针的指针变量 **ptrToPtr = %d\n", **ptrToPtr)
}

输出结果为:

a = 1
指针ptr指向地址 0xc0000a0158
指针的指针ptrToPtr指向地址 0xc0000ca018
指针 *ptr = 1
指针的指针变量 **ptrToPtr = 1

4. 指针数组与数组指针

(1)数组指针

数组指针是一个指针,指向某一个数组。
采用如下语句声明数组指针:

var arrPtr *[size]Type

示例:

func main() {
	var arrPtr *[3]int //声明数组指针arrPtr
	var arr = [3]int{1, 2, 3}
	arrPtr = &arr // 将数组 arr的地址赋值给arrPtr
	fmt.Printf("arr的内存地址arrPtr=%p\n", arrPtr)
}

运行可得输出:

arr的内存地址arrPtr=0xc0000104e0

前文提到过,*可用于从地址取值,则可用*从arrPtr中访问arr的元素,需要注意的是,若写成如下形式:

*arrPtr[0]

则程序会报错:

invalid indirect of arrPtr[0] (type int)

原因是,[]运算符优先级高于*运算符,则上述运算会计算arrPtr[0]中取出整数1,再对整数1进行*寻址,自然会报错。解决方法是用括号()调整运算的优先级:

(*arrPtr)[0]

但是在实际使用中,Golang允许我们省略*号,直接写作:

arrPtr[0]

通过如下代码进行一下测试:

fmt.Printf("arr的首元素(*arrPtr)[0] = %d\n", (*arrPtr)[0])
fmt.Printf("arr的首元素arrPtr[0] = %d\n", arrPtr[0])

输出可得:

arr的首元素(*arrPtr)[0] = 1
arr的首元素arrPtr[0] = 1

但需注意,允许省略*号的情况十分局限,通过数组指针访问数组内元素是其中一种情况。

还有另一种情况允许省略*号,即访问结构体指针的字段或者方法时,例如,有如下结构体指针:

s:=&struct{}

在访问struct下的字段或者方法时,本应采用表达式:

(*s).variable
(*s).method()

但这里golang提供了省略*号的语法糖,上述表达式可简略为:

s.variable
s.method()

(2)指针数组

指针数组是一个数组,其中每个元素都是指针,或者说都是地址值。
采用如下语句声明指针数组:

var ptrArr [size]*Type

示例:

func main() {
	a, b := 1, 2
	var ptrArr []*int = []*int{&a, &b} //构建指针数组
	for i := 0; i < 2; i++ {
		fmt.Printf("ptrArr[%d] = %p\n", i, ptrArr[i])
	}
	for i := 0; i < 2; i++ {
		fmt.Printf("ptrArr[%d]指向的值*ptrArr[i] =  %d\n", i, *ptrArr[i]) 
        //用*取出第i个指针指向的值,此处的符号*不可省略
	}
}

输出可得:

ptrArr[0] = 0xc0000121c0
ptrArr[1] = 0xc0000121c8
ptrArr[0]指向的值*ptrArr[i] =  1
ptrArr[1]指向的值*ptrArr[i] =  2

5. golang采用值传递

值传递与引用传递的概念由来已久,每当我们接触一门新的语言,一定要关注的便是其函数调用过程中传递的参数到底是值还是引用?话题展开之前,首先明确值传递与引用传递的概念:

值传递(pass by value):

在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

引用传递(pass by reference):

在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

具体到golang语言上,官方文档已经给出答案:

In a function call, the function value and arguments are evaluated in the user order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution. The return parameters of the function are passed by value back to the calling function when the function returns.

可见,golang的函数调用采用的是值传递。下面,用几个代码实例验证一下。

实例一(函数传递整型变量)

func main() {
	var a int = 10
	fmt.Printf("执行函数之前, a=%v\n", a)
	fmt.Printf("原始地址为: %p\n", &a)
	tryToChange(a)
	fmt.Printf("执行函数之后, a=%v\n", a)
}

func tryToChange(b int) {
	fmt.Printf("函数接收到的参数地址为: %p\n", &b)
	b = 20
}

得到输出:

执行函数之前, a=10
原始地址为: 0xc0000a0158
函数接收到的参数地址为: 0xc0000a0188
执行函数之后, a=10

观察上面的输出,可以发现:(1)传递给函数的值并未修改成功(2)实参a和形参b的地址并不一样。说明函数在接收到实参a后,为形参b在局部变量的栈中开辟了新的空间,并拷贝a的值,因此函数内部对形参b的任何操作都不会对实参a产生影响,故而a的值没有被修改。

实例二(函数传递指针)

func main() {
	var a int = 10
	var ptr_a *int = &a
	fmt.Printf("执行函数之前, a=%v\n", a)
	fmt.Printf("原始指针的存储地址为: %p\n", &ptr_a)
	fmt.Printf("原始指针指向的地址为: %p\n", ptr_a)
	tryToChange(ptr)
	fmt.Printf("执行函数之后, a=%v\n", a)
}

func tryToChange(ptr_b *int) {
	fmt.Printf("函数接收到的指针存储地址为: %p\n", &ptr_b)
	fmt.Printf("函数接收到的指针指向的地址为: %p\n", ptr_b)
	*ptr_b = 20
}

运行输出为:

执行函数之前, a=10
原始指针的存储地址为: 0xc000006028
原始指针指向的地址为: 0xc0000121c0
函数接收到的指针存储地址为: 0xc000006038
函数接收到的指针指向的地址为: 0xc0000121c0
执行函数之后, a=20

观察发现,(1)整数a的值修改成功(2)实参ptr_a和形参ptr_b的存储地址不一致,但是作为指针变量,两者都指向整数a的地址。这再一次印证了golang采用值传递的特性,与第一个实例相同,实参ptr_a在传递给函数后,程序会为形参ptr_b重新开辟一块地址,并将ptr_a的值(也就是整数a的地址)拷贝给ptr_b,从而程序对形参ptr_b的取值赋值操作最终都是对整数a的操作,因此在本例中a的值可以被成功修改。

实例三(函数传递slice)

应该说,在有了实例一和二之后,已经可以完整说明golang值传递特性,但是在学习golang的过程中,往往会看到这样的错误描述:“go语言中,map、slice和chan采用引用传递,其他类型采用值传递”,但这与go的文档并不一致,不难猜测,造成这样误会的原因是,当我们向函数内传入一个map或者slice,函数内的修改可以反映到实参上。实际情况如何,我们用如下代码测试一下:

func main() {
	a := []int{0, 0, 0} //创建一个切片
	fmt.Printf("原始切片地址为: %p\n", &a)
	fmt.Printf("原始切片首个元素的地址为: %p\n", &a[0])
	fmt.Printf("修改前切片首个元素为: %v\n", a[0])
	tryToChange(a)
	fmt.Printf("修改后切片首个元素为: %v\n", a[0])
}

func tryToChange(b []int) {
	fmt.Printf("接收到的切片地址为: %p\n", &b)
	fmt.Printf("接收到的切片首个元素的地址为: %p\n", &b[0])
	b[0] = 888
}

得到输出:

原始切片地址为: 0xc000004660
原始切片首个元素的地址为: 0xc0000104e0
修改前切片首个元素为: 0
接收到的切片地址为: 0xc0000046a0
接收到的切片首个元素的地址为: 0xc0000104e0
修改后切片首个元素为: 888

从输出结果可以看到,(1)原始切片a的地址和传递到函数内的切片b地址并不一致,这反映了Golang值传递的特性,(2)虽然切片自身的实参形参地址不同,但是函数内外访问的切片首位元素的地址是一样的,这是切片元素修改能够成功的原因。这两个看似冲突的现象同时出现,是因为切片在golang中可以被理解为一种特殊的引用类型,Slice类型的定义如下:

type Slice struct {
    point Point // 内存地址
    len int
    cap int
}

slice自身并不存储切片数据,也不直接指向底层数据,而是通过其point字段指向底层的数据(实际上是数组)。因此,在切片参数传递到函数中时,代码为形参b开辟新的空间,并拷贝了实参a的内容向b赋值,这导致了实参形参的地址不一致;另一方面,实参a与形参b的内容一致,所以其point字段都指向的是同一处内存空间,由此导致了在函数内修改切片元素的值能够成功。

在golang中,能够用make()函数创建的变量都可以理解做引用类型,即slice、map和chan类型。但需要注意引用类型并不意味着引用传递,在golang中只存在值传递

参考文献

https://learnku.com/articles/44096

https://learnku.com/articles/44096

https://www.flysnow.org/2018/02/24/golang-function-parameters-passed-by-value.html

posted @ 2020-12-28 10:39  lwjj  阅读(343)  评论(0编辑  收藏  举报