什么是动态数组
动态数组(Dynamic Array)是一种在需要时能够自动改变其大小的数组。与静态数组(Static Array)不同,静态数组的大小在声明时就已确定,且之后不能更改,而动态数组的大小则是根据需要动态变化的。动态数组通常通过某种数据结构(如链表、数组加额外信息等)来实现,以便在需要时能够扩展或缩小其容量。(关键字:动态扩缩容,基于链表或数组实现)
在大多数现代编程语言中,如C++的std::vector
、Java的ArrayList
、Python的列表(list)等,都提供了动态数组的功能。这些实现通常会在内部使用一个静态数组来存储元素,并跟踪当前数组的大小和已分配的容量。当向动态数组添加元素而当前容量不足时,它们会自动分配一个新的、更大的数组,并将旧数组的元素复制到新数组中,然后再添加新元素。这个过程对用户是透明的,用户无需关心底层的内存管理细节。
Go语言中的动态数组——切片(slice)
Go语言(Golang)中的切片(slice)是一种引用类型,它提供了对数组的抽象。切片是对数组一个连续片段的引用,但它比数组更灵活、更强大。以下是Go语言切片的一些主要特点:
-
动态数组:切片的大小不是固定的,它们可以根据需要增长和缩小。当向切片中添加更多元素,且切片容量不足时,Go会自动分配更大的内存空间并复制原有元素到新空间中,从而允许切片继续增长。
-
引用类型:切片是引用类型,这意味着切片变量存储的是对底层数组的引用(即内存地址),而不是数组的拷贝。因此,通过不同的切片变量操作同一个底层数组的元素时,这些操作会相互影响。
-
长度和容量:切片有两个重要的属性:长度(length)和容量(capacity)。长度是切片中元素的数量;容量是从切片的第一个元素开始到底层数组末尾的元素数量。切片的长度可以改变,但容量在切片创建时由底层数组的大小决定,且一般只能通过重新切片或使切片指向一个新的数组来改变。
-
基于数组:切片是对数组的一个连续片段的引用,但切片的使用比数组更加灵活和方便。切片可以动态地增长和缩小,而数组的大小在定义时就确定了,不能改变。
-
零值:切片的零值是
nil
,表示切片不引用任何数组。一个nil
切片的长度和容量都是0,并且没有底层数组。 -
切片操作:Go支持对切片进行切片操作,即可以从一个已存在的切片中再“切”出一个新的切片。这种操作基于原切片的底层数组,但可以有不同的长度和容量。
-
内存连续:切片的底层数组在内存中是连续的,这使得对切片中的元素进行迭代访问时效率很高。
-
内置函数支持:Go的标准库提供了许多内置函数来操作切片,如
append
用于向切片追加元素,copy
用于切片间的元素复制,以及len
和cap
用于获取切片的长度和容量等。
切片数据结构
type slice struct {
// 底层数组指针(指向一块连续内存空间的起点)
array unsafe.Pointer
// 切片长度
len int
// 切片容量
cap int
}
cap>=len
切片的创建和初始化
//通过字面量创建
/**
创建字符串切片,长度和容量都是3
*/
slice1 := []string{"aaa","bbb","ccc"}
fmt.Println(slice1) //[aaa bbb ccc]
/**
创建整形切片,长度和容量都是4
*/
slice2 := []int{10,20,30,40}
fmt.Println(slice2) //[10 20 30 40]
//当使用切片字面量创建切片时,还可以设置初始长度和容量。
/**
创建一个长度和容量都是100的切片 索引0 = 10,索引99 = 1
*/
slice3 := []int{0:10,99:1}
fmt.Println(slice3) //[10 0 0 ... 1]
//通过 make() 函数创建切片
/**
创建长度和容量都是5的切片
*/
slice4 := make([]int,5)
fmt.Println(slice4) //[0 0 0 0 0] 默认对应类型的零值
/**
创建长度=3,容量=5的切片
*/
slice5 := make([]string,3,5)
fmt.Println(slice5) //[ ]
/**
不允许创建容量小于长度的切片
*/
slice6 := make([]int,5,3)
fmt.Println(slice6) //invalid argument: length and capacity swapped
切片初始化源码
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
- math.MulUintptr 方法计算出初始化切片需要的内存和空间大小
- 空间超限,直接panic
- mallocgc方法,为切片进行空间分配
切片在方法之间的传递
func main() {
sl := []int{1,2,3,4,5}
fmt.Println(sl) //[1 2 3 4 5]
change(sl)
fmt.Println(sl) //[10 2 3 4 5]
}
func change(sl []int) {
sl[0] = 10
}
每次在方法之间传递切片时,会将切片实例本身进行一次值拷贝(也可以叫浅拷贝),会将array指针,len长度,cap容量进行拷贝所以方法中和方法外的切边array指向同一片内存空间,因此在局部方法中执行修改操作时,还会根据这个地址信息影响到原 slice 所属的内存空间,从而对内容发生影响;
但是如果在函数中的切片发生了扩容,此时内外就是两个独立的切片;
截取切片
在Go语言中,可以通过指定起始索引和结束索引来截取一个新的切片(包前不包后);
slice[start:end]
//slice要截取的原始切片
//start截取切片的开始位置,不填默认为0
//end截取切片的结束位置,不填默认为切片长度
s := []int{1, 2, 3, 4, 5}
// 截取整个切片
subS1 := s[:]
fmt.Println(subS1) // 输出: [1 2 3 4 5]
// 截取索引1到4(不包含4)的元素
subS2 := s[1:4]
fmt.Println(subS2) // 输出: [2 3 4]
// 从索引0开始截取到索引4(不包含4)
subS3 := s[:4]
fmt.Println(subS3) // 输出: [1 2 3 4]
// 从索引2开始截取到切片末尾
subS4 := s[2:]
fmt.Println(subS4) // 输出: [3 4 5]
截取出的切片和原始切片的关系
在对切片 slice 执行截取操作时,本质上是一次引用传递操作,因为不论如何截取,底层复用的都是同一块内存空间中的数据,只不过,截取动作会创建出一个新的 slice header 实例;
s := []int{2,3,4,5}
s1 := s[1:]
往切片中新增元素(append)
通过append方法可以实现在切片的末尾添加元素
func main() {
s := []int{1, 2, 3}
// 使用 append 函数在切片末尾添加元素 4
s = append(s, 4)
fmt.Println(s) // 输出: [1 2 3 4]
}
func main() {
s := []int{1, 2, 3}
// 使用 append 函数在切片末尾添加多个元素
s = append(s, 4, 5, 6)
fmt.Println(s) // 输出: [1 2 3 4 5 6]
}
func main() {
s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
// 使用 append 函数将 s2 添加到 s1 的末尾
s1 = append(s1, s2...) // 注意 s2 后的 ... 用于将 s2 展开为元素序列
fmt.Println(s1) // 输出: [1 2 3 4 5 6]
}
func main() {
s := []int{1, 3, 4, 5}
// 在索引 1 的位置插入元素 2
// 首先,将索引 1 及其之后的元素向后移动一位
s = append(s[:1], append([]int{2}, s[1:]...)...)
fmt.Println(s) // 输出: [1 2 3 4 5]
}
删除切片中的元素
在Go语言中,切片(slice)本身并不提供直接的删除元素的方法,因为切片是对底层数组的抽象,而数组的大小是固定的。但是,通过以下几种方式来实现“删除”切片中的元素;
//使用切片操作删除元素(适用于删除末尾元素)
s := []int{1, 2, 3, 4, 5}
// 删除切片末尾的元素
s = s[:len(s)-1]
fmt.Println(s) // 输出: [1 2 3 4]
//使用append和切片操作删除任意位置的元素
s := []int{1, 2, 3, 4, 5}
// 假设我们要删除索引为2的元素(值为3)
index := 2
// 将index之后的元素(不包括index)复制到前面,然后截断切片
s = append(s[:index], s[index+1:]...)
fmt.Println(s) // 输出: [1 2 4 5]
//使用循环和条件语句删除满足条件的元素
s := []int{1, 2, 3, 4, 5}
// 删除所有值为3的元素
var newS []int
for _, value := range s {
if value != 3 {
newS = append(newS, value)
}
}
s = newS
fmt.Println(s) // 输出: [1 2 4 5]
拷贝切片
slice 的拷贝可以分为简单拷贝和完整拷贝两种类型;
要实现简单拷贝(浅拷贝),我们只需要对切片的字面量进行赋值传递即可,这样相当于创建出了一个新的 slice header 实例,但是其中的指针 array、容量 cap 和长度 len 仍和老的 slice header 实例相同;
slice 的完整复制(深拷贝),指的是会创建出一个和 slice 容量大小相等的独立的内存区域,并将原 slice 中的元素一一拷贝到新空间中,在实现上,slice 的完整复制可以调用系统方法 copy;
s := []int{1, 2, 3}
// 分配一个新的切片,长度和容量与s相同
sCopy := make([]int, len(s))
// 使用copy函数复制元素
copy(sCopy, s)
// 现在sCopy是s的一个独立拷贝
fmt.Println(sCopy) // 输出: [1 2 3]
切片的扩容
什么时候会触发扩容
当 slice 当前的长度 len 与容量 cap 相等时,下一次 append 操作就会引发一次切片扩容;
扩容步骤
1.计算目标容量
case1:如果新切片的长度>旧切片容量的两倍,则新切片容量就为新切片的长度case2:
- 如果旧切片的容量小于256,那么新切片的容量就是旧切片的容量的两倍
- 反之需要用旧切片容量按照1.25倍的增速,直到>=新切片长度,为了更平滑的过渡,每次扩大1.25倍,还会加上3/4* 256
2.进行内存对齐
需要按照Go内存管理的级别去对齐内存,最终容量以这个为准