社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
数组和切片是我们较为常用的数据结构,正是因为他们的普遍,其中有许多小问题都被我们忽略掉了,比如说:
1.在作为参数传递时,为什么切片的改动会使得原来的实参也发生变化,而数组的改动就不会影响到实参
2.为什么数组的类型会因为长度不同而无法兼容,但是切片却可以随意传递,两者的内存布局到底是什么样的
3.为什么数组偏向静态的不能追加,而切片却可以动态扩容
…
先从定义开始
数组常用的定义方法:
var arr_1 = [...]int{1,2,3}//[...]int只能在定义时用,不能在返回时参数,和形参使用
var arr_2 [1]int//在定义时,这个容量必须是恒定数字,不能是一个int变量,不然报错:x:=1, arr:=[x]int就不能
通用结构就是:
var 名称 [数组长度]元素类型
var arr [3]int
切片的定义方法:
var slice_1 = make([]int,10,10)//通过make创建
var slice_2 []int = arr_1[0:3]//截取数组的相关片段
通用结构:
var 名称 [ ]元素类型
var slice [ ]int
其实最快了解数组和切片的方式就是掌握他们的内存布局
我们先打印一下上面的数组和切片的相关数据:
fmt.Printf("arr1首地址:%pn,arr1【0】首地址:%pn,slice首地址:%pn,slice【0】地址%pn",&arr_1,&arr_1[0],&slice_1,&slice_1[0])
/*arr1首地址:0xc0420480a0
arr1【0】首地址:0xc0420480a0
slice首地址:0xc0420443e0
slice【0】地址0xc0420820a0
那么问题来了,为什么数组的首地址和他头号元素的地址相同,而切片的首地址和他头号元素不同呢???
其实是这个样子的:
首先我们来看看数组的内存结构:
数组其实就一段大小固定的空间,他的大小就是在初始化时给的长度len,根据长度len和元素类型大小(例如元素为int64也就是8字节)就可以很轻易的算出空间的完整大小,在完全确定后就会直接敲定无法改变。他的头号元素也是整个空间的头,地址一致。
正因如此,数组具有以下特点:
1.长度固定,无法追加元素,因为空间大小就给了这么多,所以[5]int和[10]int两个数组表示的是不同的类型自然无法兼容
2.数组是值类型的,在传递时采用值拷贝:
func test(arr [3]int){//数组类型参数不能写成[...]int,这个只能用于定义时,且不能写[]int,这是切片类型
fmt.Printf("%p",&arr)//参数地址:0xc042048160,,arr1首地址:0xc0420480a0,两者不同且修改后不影响
for index,value:=range arr{
arr[index] -= value
}
fmt.Println("清零数组:",arr)//数组由于是定死的,所以也是值拷贝,只会在这个方法对应的栈帧里的数组进行一个操作,不会影响外边
}
var arr_1 = [...]int{1,2,3}
test(arr_1)//数组值拷贝,我在里面清零后,原数组不变
fmt.Println("源数组:",arr_1)
/*清零数组: [0 0 0]
源数组: [1 2 3 ]*/
看结果就能证明这一点,其实本质还是在main中调用test方法,该方法栈帧入了栈后将实参arr1传过去,打印发现地址不同但是修改完成了后,实参并未受到影响。
(这里的意思就是arr1是实参,调用函数test传递的arr是函数对应的形参,他相当于从开了一片空间,把arr1的空间中的值给复制一份到arr空间然后传递,所以从内存分布上来讲,他俩肯定空间不同,那地址也必然不同,当参数传过来后,通过修改形参并没影响到实参,那就说明必然是值传递),所以要想修改实参中的数组数据,就只能传地址过去。
切片的内存布局
切片本质就是一个结构体,他里面包含三部分:address + len + cap,
address: 就是他指向的内部数组或数组某个地方
len:是当前的元素个数
cap:可容纳元素总容量大小
也不难理解为什么cap>=len
正是如此,切片本质上是一个引用空间,该空间和元素空间完全是两个空间,所以切片的首地址和头号元素的首地址完全不同。
func test_3(a []int){
fmt.Printf("%p",&a)//0xc042002440
a[0] = 999
}
var slice_1 = make([]int,10,10)
test_3(slice_1)
fmt.Printf("%p",&slice_1)//0xc042002420
fmt.Println(slice_1)//[999 0 0 0 0 0 0 0 0 0]
我们可以看到,实参slice_1在函数调用时作为参数传过去了,而a的地址和slice_1地址不同,也就是两个不同的空间,但是对a进行操作后,最终打印slice_1我们发现他的值也变化了,故slice_1空间的内容复制给a空间的是地址值,然后a对该地址指向的空间进行操作也会影响到slice_1,所以是引用传递。
值得一提的是:通过make创建的切片和通过数组[:]分的切片是有区别的!
1.直接var slice_2 []int = arr_1[1:3]切分数组产生的切片其实就是直接对这个数组的引用,这时当切片中的值修改时会影响到原来的数组值:
var arr_1 = [...]int{1,2,3}
var slice_2 []int = arr_1[1:3]
fmt.Println(arr_1)//[1 2 3]
fmt.Println(slice_2)//[2 3]
test_3(slice_2)
fmt.Println(slice_2)//[999 3]
fmt.Println(arr_1)//[1 999 3]
2.而通过make创建其实本质上也是先偷偷的创建一个内部数组,然后该切片在对该数组进行操作。
var slice_1 = make([]int,10,10)
xx := slice_1
fmt.Println(slice_1)//[0 0 0 0 0 0 0 0 0 0]
xx[0] = 260
fmt.Println(slice_1)//[260 0 0 0 0 0 0 0 0 0]
fmt.Printf("xx空间地址:%pn slice1地址%pn xx[0]%pn,slice[0]%pn",&xx,&slice_1,&xx[0],&slice_1[0])
/* xx空间地址:0xc042044420
slice1地址0xc0420443e0
xx[0]0xc0420820a0
slice[0]0xc0420820a0
不难看出,把slice1赋值给新生成的xx,他俩虽然不是同一空间,但是里面内容一致,且指向同一个内部数组,当xx修改0时,slice的也变了
xx := slice1 赋值过去本质就是把xx空间内容赋值给xx,就是把它里面的add+len+cap给赋值过去xx,所以俩add一致维护同一个数组
*/
1.len(数组/切片):返回元素个数,也就是长度
fmt.Println(len(arr_1),len(slice_1))//3 10
2.cap(数组/切片):返回总容量大小
fmt.Println(cap(arr_1),cap(slice_1))//3 10
3.append():给切片追加元素
var slice_1 = make([]int,10,10)
var slice_2 []int = arr_1[1:3]
var slice_3 []int= append(slice_2,slice_1...)//俩切片互相追加,则必须加...
fmt.Println(slice_3)
fmt.Println(len(slice_1),cap(slice_2),len(slice_3),cap(slice_3))
/* [2 3 0 0 0 0 0 0 0 0 0 0]
10 2 12 12*/
对切片追加会判断是否已经超了cap,是的话就会扩容
slice_2 = append(slice_2,slice_1...)
fmt.Println(slice_2)//[3 4 0 0 0 0 0 0 0 0 0 0]
扩容本质就是底层从新创建一个满足当前大小cap的数组(新空间),并且把原数组中的值给拷贝过来,在让切片指针指向新数组。
这个也很好证明,因为刚才已经证明过了:操作slice_2【0】的时候对slice_2修改会影响到arr1,现在给slice_2扩容了后再操作他,看看会不会对arr1产生影响:如果扩容后修改slice_2【0】仍然会使得arr1【1】变化,那就说明仍然是原数组空间扩容。如果不会是的arr【1】变化,则证明扩容后的数组是新数组而不是原数组
var arr_1 = [...]int{1,2,3}
var slice_1 = make([]int,10,10)
var slice_2 []int = arr_1[1:3]
slice_2 = append(slice_2,slice_1...)
fmt.Println(arr_1)//[1 2 3 ]
fmt.Println(slice_2)//[2 3 0 0 0 0 0 0 0 0 0 0]
test_3(slice_2)
fmt.Println(slice_2)//[999 3 0 0 0 0 0 0 0 0 0 0]
fmt.Println(arr_1)//[1 2 3]
我们可以发现这次arr1的值并没有发生改变,也就是产生了新数组,然后slice_2的引用指向了新数组。
1.数组是静态定死的len不可变,而切片是动态的可以继续扩容追加
2.数组的类型是[len]int,且不同len长度的数组相互不兼容,而切片是稳定的[]int类型
3.数组是值传递,空间内容就是数组的每个元素,他的len也就是cap; 切片是引用传递,其空间内容:address + len + cap,容量可以扩容且>=len
如有问题欢迎指出,共勉互进!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!