切片
1. 切片的定义
切片(slice)是Golang中独有的数据类型。
数组有特定的用处,但是有不足之处 :运行时长度不可变。切片是随处可变的,它构建在数组之上,并且提供更强大的能力和便捷。
切片(slice)是对数组一个片段的引用,所以切片是引用数据类型。这个片段可以是整个数组,也可以是数组的一部分。
特点 :
-
切片与其引用的数组共享内存地址
-
虽然切片可变大,但是不能超过其引用的数组,使用cap(切片)的计算切片的长度。
0 <= len(切片) <= cap(切片) <= len(arr)
-
切片是数组的引用,但它是一个结构体,值传递。
切片的容量如何计算 :
切片容量为切片起始位置到数组结束位置的元素个数。假如从数组下标n开始切片,那么 cap(切片) = len(数组) - n
2. 切片的声明
2.1 空切片
声明一个数组,不指定其大小,该数组就是一个切片。
这个切片是一个空切片,它的值是 nil
var arr[]int
if arr == nil {
fmt.Println("切片的元素数:", len(arr))// 0
fmt.Println("切片的容量数:", cap(arr))// 0
fmt.Println("切片的所有元素:", slice1)// []
fmt.Println("arr是空切片")// arr是空切片
}
2.2 下标创建切片
切片名 := 数组名[起始索引, 结束索引]
一旦这样声明,切片就会将数组 [****起始下标,结束下标**)**中的值引用一份。
var arr [10]int = [10]int{1, 2, 3, 4, 5, 6, 7}
// slice就是一个切片,slice将 1~2 下标的值复制一份。
// 这个slice中的元素为: [2, 3]
var slice []int = arr[1:3]
-
如果缺起始下标,默认从数组开头区域截取。
-
如果缺结束下表,默认从截取至数组结束。
len(切片) :查看切片的大小
len(slice)
cap(切片) :查看切片的容量
cap(slice)
// 0 <= len(slice) <= cap(slice) <= len(arr)
看看切片地址与数组地址到底一样不一样 :
使用的下标必须是非负且在范围内的,若超过范围,会出现 panic
2.3 make创建切片
name := make(Type, length, capacity)
切片名 := make(切片类型,切片长度,切片长度)
// 定义一个切片,类型为int数组, 长度为5, 容量为20
slice := make([]int, 5, 20)
该切片最大扩容到 长度 = 容量 = 20。
你是否会有疑惑,不是说切片是数组的引用,必须引用一个数组吗?
For example, make([]int, 0, 10) allocates an underlying array of size
and returns a slice of length and capacity that is backed by this underlying array
关于make的官方解释如上所示 :声明一个底层数组,返回关于这个数组的切片。 将数组隐藏起来,通过切片操作数组。
切片定义后跟正常的数组使用一样,可以通过 变量名[下标] 进行操作。
slice2 := make([]int, 5, 20)
fmt.Println(slice2) //此时为空切片
slice2[0] = 1
slice2[1] = 23
slice2[2] = 32
slice2[3] = 154
slice2[4] = 6
fmt.Println(slice2) //此时已经有值
3. 切片的底层
切片的底层是一个结构体,有三个变量 :指针
、长度
、容量
4. 切片的注意事项
-
切片可以继续切片,多个切片指向同一块区域,会互相影响。
-
切片不能直接比较,无法用
==
判断两个切片是否相等,它唯一的等值操作用于和nil
比较。一个nil的切片,底层指针是无指向的,并且长度、容量都为0。
slice == nil
len(slice) = 0
cap(slice) = 0
一个指向0数组的切片,底层是有指向的,长度、容量都为0
slice != nil
len(slice) = 0
cap(slice) = 0
所以不能使用 切片是否为nil 来判断切片是否为空,应该判断 len(切片) 是否为0
5. 切片的扩容和拷贝
上文说过,切片是对数组的引用,可以扩容,那么它是如何扩容的?切片会自动扩容吗?
尝试以下能否自动扩容 :
// 声明一个长度为5,容量为20的切片
slice2 := make([]int, 5, 20)
// 将前五个空间占满
slice2[0] = 1
slice2[1] = 23
slice2[2] = 32
slice2[3] = 154
slice2[4] = 6
// 给第六个空间赋值
slice2[5] = 6
发现运行出现bug,证明无法自动扩容。
那么该如何扩容 ?使用 append()
5.1 append()
func append (slice []Type, elems …Type) []Type
第一个参数是想要扩容的切片。
第二个参数是给这个切片扩容的元素,要与原切片中的元素类型相同。
返回值是扩容后的切片。
slice2 := make([]int, 5, 20)
slice2[0] = 1
slice2[1] = 23
slice2[2] = 32
slice2[3] = 154
slice2[4] = 6
// 这样是错误的: slice2[5] = 6
slice3 := append(slice2, 6)
fmt.Println(slice3)
这样就给切片扩容了一个元素 :6
切片的扩容策略 :
-
如果新申请的容量(cap)大于2倍的旧容量(old.cap),最终的容量(newcap)就是新容量
-
否则判断 旧容量(old.cap)的长度小于1024,如果小于,最终容量(newcap)就是旧容量(old.cap)的2倍
-
如果大于等于1024,最终容量(newcap)从旧容量开始循环增加 (newcap + 256*3) / 4,直至最终容量(newcap)大于等于新容量(cap)
注意 :循环增加多少,这个要看Go语言版本,不同版本可能不同。并且扩容规则针对不同类型也有不同的实现。
func growslice(et *_type, old slice, cap int) slice {
// 此处省略一大段判断条件...
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += (newcap + 3*threshold) / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
// 下面省略一大段判断条件
}
新申请的容量(cap)大于2倍的旧容量(old.cap)
slice2 := make([]int, 0, 1)
slice2 = append(slice2, 1, 2, 3, 4, 5, 6)
fmt.Println("slice2的长度为:", len(slice2))
fmt.Println("slice2的容量为:", cap(slice2))
容量小于1024,最终容量是旧容量的二倍
slice5 := make([]int, 0, 1)
for i := 0; i < 10; i++ {
slice5 = append(slice5, i)
fmt.Printf("切片的长度为: %d\t\t", len(slice5))
fmt.Printf("切片的容量为: %d\n", cap(slice5))
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7rDoNM89-1673752243237)(null)]
累了,另外一个不想算了…🥲
5.2 copy()
当多个切片指向同一个数组时,它们操作共享空间,彼此影响。这时需要使用拷贝函数 copy() 对切片进行值拷贝。
func copy(dst, src []Type) int
dst :目标切片
src :数据来源切片
返回值 :拷贝了多少个数据过去
slice2 := make([]int, 5, 20)
slice2[0] = 1
slice2[1] = 23
slice2[2] = 32
slice2[3] = 154
slice2[4] = 6
slice4 := make([]int, 5, 5)
i := copy(slice4, slice2)
fmt.Println("copy的返回值为:", i) // 5
fmt.Println("slice4的值为:", slice4) // [1 23 32 154 6]
copy也被称为深拷贝,直接将一个切片指向另一个切片叫浅拷贝