前言
最近做项目,使用go开发,但是在发生函数调用传参数时,对指针的指针的传递有难以理解的代码,就此分析过程。尤其是对于多重指针作为参数,而且对于一些内置函数的修改逻辑也需深入的理解。
1. demo
package slice_demo
import (
"fmt"
"testing"
)
func TestSliceCall(t *testing.T) {
s := make([]int, 1, 10)
fmt.Printf("1.---- %p %p %v\n", &s, s, s)
a := append(s, 2, 3)
fmt.Printf("1.---- %p %p %v\n", &a, a, a)
changeSlice(a)
changeSlicePoint(&a)
fmt.Println(a, s)
}
func changeSlice(b []int) {
b = append(b, 5, 6)
b[0] = 111
fmt.Printf("2.---- %p %p %v\n", &b, b, b)
}
func changeSlicePoint(p *[]int) {
*p = append(*p, 10, 12)
fmt.Printf("3.---- %p %p %p %v\n", &p, p, *p, p)
}
从demo看,有2种方式传参数修改切片的值,有下标修改,有append修改,结果有点出乎我们日常逻辑,结果如下
=== RUN TestSliceCall
1.---- 0xc000118090 0xc000154140 [0]
1.---- 0xc0001180d8 0xc000154140 [0 2 3]
2.---- 0xc000118120 0xc000154140 [111 2 3 5 6]
3.---- 0xc000100028 0xc0001180d8 0xc000154140 &[111 2 3 10 12]
[111 2 3 10 12] [111]
--- PASS: TestSliceCall (0.00s)
PASS
可以看到append后的指针跟append前的切片是一样的,切片本身是引用类型,实现逻辑是数组,且是同一个数组(没扩容,且内存地址一样),但是值不一样。
而且切片在传入函数参数后,通过append并没有修改 传入的参数切片引用,可以通过下标修改;但是通过切片指针通过append却可以修改,原理是什么😅
2. 分析
2.1 先分析append的情况
结合这2行打印分析,append的过程
可以看到创建了2个变量(栈的引用) ,所以分配了2个内存地址,同时指向同一个数组(因为切片本身是指针),然而s和a的值却不一样。那么这是什么原因?实际上数组的内存地址仅仅是数组开始的内存地址,数组是连续的内存空间,是一个初始地址+offset或者position定位。看slice结构体可以明白,实际上内存地址后面的值已经被改掉了。
2.2 slice的结构体
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice.go定义了结构体,实际上slice是数组指针与len cap组成的,slice的扩容就是创建复制数组 ,修改len和cap。
2.3 slice直接传入函数参数
实际上append未生效,并不是未生效,已经修改了数组后面的连续内存的值了,只是因为没有对原始的变量赋值,没有修改len与cap,因为切片的结构体slice是基本数据类型,内存拷贝,是2个结构体,所以读取值是没变的。
分析原理画成图,跟实际结果符合。
2.4 slice传入函数指针
实际上切片没必要使用指针,因为切片本身就是指针,很多教程也推荐切片不使用指针,直接用;但是指针却有特殊的意义,可以修改传入的参数的数据值。
因为传入的切片的指针,所以p可以认为是a,因为是a的指针内存地址,操作a的指针内存地址就等于操作变量a,所以对*p赋值,可以修改a的值。
2.5 切片操作相互影响
因为切片的实现是数组,因为是指针,所以变量的修改会相互影响。这个就很容易理解了
总结
这里的问题是切片本身是指针,如果再加入指针,就是指针的指针,很难理解。而且再结合函数的参数,本身函数的参数是一个引用,栈变量自己又会分配内存地址,就更难理解了😅。这里的关键还有切片是一个结构体存储的,但是结构体又是内存值拷贝,而非内存地址引用。实际上可以结合内存分配的流程结合函数的入栈出栈,外加参数的存储结构很容易就明白原理了。