相同和差异
数组和切片都是集合类型,都是可以用来存储某一类数据结构 但是数组的长度是固定的,切片的长度是可变的。 切片属于引用数据类型, 数组属于值类型。
关系探讨
切片可以看作是对数组的又一次封装,在切片的底层数据结构还是数组,切片就可以看作是是对数组内某个连续片段的引用。 len 和cap 函数对数组和切片的结果也不同。 在数组中, 数组的长度len 等于数组的容量cap 。 切片的长度则表示对底层数组的引用的连续片段的长度,容量则是底层数组的容量。 看一下通过通过make声明数组的结果
func main() {
slice1 := make([]int, 4)
slice2 := make([]int, 4, 8)
fmt.Println("array len is", len(slice1))
fmt.Println("array cap is", cap(slice1))
fmt.Println("slice len is", len(slice2))
fmt.Println("slice cap is", cap(slice2))
}
以上案例输出为:
slice1 len is 4
slice1 cap is 4
slice2 len is 4
slice2 cap is 8
结果说明如果在声明切片的时候不指定容量, 那么切片的容量就等于切片的长度。 同时使用make切片的引用是从底层数组的index为0元素开始的。 现在slice2 和底层数组的关系如下图
那么我们append一下slice2 , 代码
func main() {
slice1 := make([]int, 4)
slice2 := make([]int, 4, 8)
fmt.Println("slice1 len is", len(slice1))
fmt.Println("slice1 cap is", cap(slice1))
fmt.Println("slice2 len is", len(slice2))
fmt.Println("slice2 cap is", cap(slice2))
slice3 := append(slice2, 1)
fmt.Println("slice3 len is", len(slice3))
fmt.Println("slice3 cap is", cap(slice3))
}
结果:
slice1 len is 4
slice1 cap is 4
slice2 len is 4
slice2 cap is 8
slice3 len is 5
slice3 cap is 8
可以看到slice3 的len 是5,但是cap 依然是8。 说明底层数组并没有发生变化。 如果append导致len 超过8呢
func main() {
slice1 := make([]int, 4)
slice2 := make([]int, 4, 8)
fmt.Println("slice1 len is", len(slice1))
fmt.Println("slice1 cap is", cap(slice1))
fmt.Println("slice2 len is", len(slice2))
fmt.Println("slice2 cap is", cap(slice2))
for i := 0; i < 5; i++ {
slice2 = append(slice2, 1)
}
fmt.Println("slice2 append len is", len(slice2))
fmt.Println("slice2 append cap is", cap(slice2))
}
结果:
slice1 len is 4
slice1 cap is 4
slice2 len is 4
slice2 cap is 8
slice2 append len is 9
slice2 append cap is 16
发现底层数组的容量翻倍了,由于数组的容量和长度不可变,是切片又申请了一个新的底层数组来使用。
切片的切割
如果在原有的切片上截取新的切片, 那么新的切片使用的还是原来切片的数组吗? 看如下代码:
func main() {
sliceAll := []int{1, 2, 3, 4, 5, 6, 7, 8}
slice := sliceAll[2:6]
fmt.Println("slice len is", len(slice))
fmt.Println("slice cap is", cap(slice))
sliceAll[2] = 88
fmt.Println("slice cap is", slice[0])
}
结果:
slice len is 4
slice cap is 6
slice cap is 88
新的切片的容量不是8, 同时我们改变了原有切片的一个数字, 新的切片也改变了,说明两个切片依然是引用的同一个底层数组。
因为切片是无法向左拓展的,左边的数据没用,所以新的切片引用不是从index为0开始的,是从index为2开始的,如下图红框内部所示, 那么这个时候容量就是6了。说明切片的容量并不是一直等于底层数组的容量,而是引用起始的index到数组末尾的容量。 如果这个时候切片元素满了会怎么办,是基于切片的容量还是底层数组的容量来扩容呢?代码如下:
func main() {
sliceAll := []int{1, 2, 3, 4, 5, 6, 7, 8}
slice := sliceAll[2:6]
fmt.Println("slice len is", len(slice))
fmt.Println("slice cap is", cap(slice))
sliceAll[2] = 88
fmt.Println("slice cap is", slice[0])
for i := 0; i < 3; i++ {
slice = append(slice, 99)
}
fmt.Println("slice cap is", cap(slice))
fmt.Println("sliceAll cap is", cap(sliceAll))
}
结果:
slice len is 4
slice cap is 6
slice cap is 88
slice cap is 12
sliceAll cap is 8
发现是基于6翻倍的,说明切面扩容不是基于底层数组的容量, 而是基于切片的容量,还有在申请新的数组的同时并没有改变原来切片sliceAll 的引用的数组,原有切片的容量还是8。
总结
切片是基于数组的一次封装, 解决了数组长度不可变的问题,但是其底层存储的实现依然是基于数组操作,尤其是容量不足的时候,会再次申请新的数组作为存储使用。 所以再切片的使用过程中如果可以提前确定容量,最好是声明容量创建,因为每次扩容,新的空间的申请和数据复制时比较消耗性能的。
|