之前的切片探索中,上篇通过一道算法题目,了解到切片的两大特性:一是:切片是引用类型,指向底层数组,修改其底层数组的时候,会影响切片中的值。二是:向切片中添加元素的时候,切片可能会发生扩容,改变其底层指向的数组。在下篇中,我们谈到了切片的底层实现原理以及扩容机制。在之后的学习中,切片的应用场景比较多,自己也有了一些新的发现,于是形成补充篇这个文章。接下来我会从切片的几种创建方式说明切片和底层数组之间的关系、切片作为函数的参数时,实际上传递的是什么?
上下两篇:
1.GO语言-切片底层探索(上)-CSDN博客
2.GO语言-切片底层探索(下)-CSDN博客
1. 查看切片底层依托的数组
func main() {
//切片slice1基于array数组创建的
array := [7]int{1, 2, 3, 4, 5, 0, 1}
slice1 := array[3:5]
//查看array数组的信息 [1 2 3 4 5 0 0] 7 7
fmt.Println(array, len(array), cap(array))
//查看slice1切片的信息 [4 5] 2 4
fmt.Println(slice1[:], len(slice1[:]), cap(slice1[:]))
//查看slice1切片依托底层数组的信息 [4 5 0 1] 4 4
fmt.Println(slice1[:cap(slice1)], len(slice1[:cap(slice1)]), cap(slice1[:cap(slice1)]))
}
在上面的代码中,我们发现一个奇怪的现象:当我们使用切片表达式slice1[:]和slice1[:cap(slice1)]所取到的值是完全不同的。
我们常用到的切片表达式是slice[low:high]这种两个参数的形式,我们称其为简单切片表达式。此外还有一种三个参数形式的切片表达式,我们称其为扩展切片表达式,但是一般不经常使用,这里权当扩展一下。在简单切片表达式中,切片的长度length = high-low,切片的容量cap = 底层数组的长度-low。这里的low和high都是可以省略的,如果省略low则默认为0,如果省略high则默认为切片的长度,而不是容量。
slice1[:]实际上取的是切片的范围,也就是从切片的下标0到切片的长度-1。而slice1[:cap(slice1)]底层数组的范围,从slice1所依托得底层数组从下标low(这里是3,因为我们的slice1= array[3:5]),到底层数组末尾。
虽然我们在项目中一般不使用slice[:cap(slice)]这种形式,但是我们知道如果通过获取切片所依托的底层数组的方法,可以帮助我们更加清晰地理解切片和底层数组的关系,以及切片中的len和cap实际代表的是什么!
2. 切片和底层数组的关系
- 直接赋值方式 slice:= []int{1,2,3,4,5}
- make字面量方式 slice:= make([]int,0,10)
- 使用切片表达式根据切片或数组生成 slice:= array0[1:3]
通过之前的文章,我们知道,切片是依托于数组实现的,相比于数组而言,切片在容量不足的时候,会进行自动扩容,更具有灵活性。我们一般都是通过以上三种方式创建切片的,这三种不同的创建方式,将形成三种不同的(切片和其底层数组之间的)关系。
- 直接赋值创建方式,这种方式创建出来的切片长度等于容量,此时如果我们向切片中添加一个新的元素,就会触发扩容机制,改变切片指向的底层数组。
- make字面量方式 slice:= make([]int,0,10),通过make创建切片,可以指定切片的长度和容量(底层数组的长度),后续向切片中添加元素的个数,如果没有超过10就不会发生扩容。通过提前指定切片的容量,可以减少程序运行过程中,切片扩容带来的资源消耗。
- 使用切片表达式根据切片或数组生成 slice:= array0[low:high]。使用切片表达式创建的切片,其长度为high-low,容量是底层数组的长度-low。
3. 切片作为函数的参数时,传递的是对底层数组的引用
func main() {
baseArray := [5]int{1, 2, 3, 4, 5}
//基于baseArray创建slice
slice := baseArray[:]
modifySlice(slice, 0)
fmt.Println(slice, len(slice), cap(slice)) // 输出 [100 2 3 4 5]
fmt.Println(baseArray, len(baseArray), cap(baseArray)) // 输出 [100 2 3 4 5]
}
func modifySlice(slice []int, index int) {
slice[index] = 100
}
在代码中,我们基于baseArray创建slice,我们发现当传入的切片在函数中修改时,其依赖的底层数组也发生了修改。这说明,切片作为函数的参数时,实际上传递是对底层数组的引用。如果我们在函数的操作导致切片进行了扩容,那么我们的底层数组中的值将不会再发生变化了。
测试如下:
func main() {
baseArray := [5]int{1, 2, 3, 4, 5}
//基于baseArray创建slice
slice := baseArray[:]
modifySlice(slice, 0)
fmt.Println(slice, len(slice), cap(slice)) // 输出 [100 2 3 4 5]
fmt.Println(baseArray, len(baseArray), cap(baseArray)) // 输出 [100 2 3 4 5]
}
func modifySlice(slice []int, index int) {
slice[index] = 100 //baseArray[index]被修改
slice = append(slice, 1) //扩容,底层数组改变
slice[index] = 1000 //baseArray[index]值不变
fmt.Println(slice, len(slice), cap(slice)) //[1000 2 3 4 5 1] 6 10
}
4. 总结
在这篇博客中,我们主要讲解切片和底层数组之间的关系,并且通过切片的三种创建方式来进行详细的说明。我们在使用切片的时候,一定要注意切片扩容后,其底层指向的数组会发生变化,对切片的修改将不再作用与原来的底层数组。