【From C To Go】 1.2 数组与切片

0 个人理解

数组的核心概念是具有固定长度且相同类型的在内存中连续的数据序列,这句话其实不难理解,我来做一些说明:

  1. 固定长度:在大多数编程语言中,数组的长度是在定义时便固定好的,这主要是为了申请内存,不管是在静态区、栈还是堆,当然,数组长度在一开始便固定下来不见得是一件好事,你实际需要的长度未必与定义数组时声明的长度等价,如果你声明的数组长度太大,那势必会造成空间的浪费,否则,当需要添加的元素个数超出数组固定长度时,如何处理后面的元素也是大老难的问题。因此,C++有vector,java有ArrayList,Go语言有Slice(切片),都是为了解决数组长度是固定的这一问题的产物
  2. 相同类型:上面提到,数组固定长度是为了方便申请内存,相同类型这一特性也是为了此目的,当然,一个数组里面的数据类型都是相同的也更符合人类直觉,否则便是python中的元组之类的概念了。总而言之,当我们提出我们需要某种类型的长度为多少的数组的需求之后,编辑器就可以计算出来我们需要多少连续的内存
  3. 内存中连续:在与链表对比时,数组的优势便是随机访问元素的时间复杂度是O(1),这主要得益于其在内存中连续的特性,当我们需要找到数组的第n个元素,即arr[n-1],只需要将数组首地址向后偏移n * 元素类型大小即可定位到对应元素

总的来说,数组是一种不够灵活但在某些操作时又比较方便的数据类型,而切片是为了方便对数组进行操作的一种新型数据结构,接下来总结一下Go语言中这两者的用法

1 Go数组

1.1 定义和初始化

1.1.1 定义数组

  • 定义数组需指明元素类型数组长度
  • 未初始化的数组,其元素会被初始化为对应元素类型的默认值
	// 定义数组
	var arr [5]int
	fmt.Println(arr)

该数组的输出为[0 0 0 0 0]

1.1.2 初始化

数组的初始化有以下三种方式:

  1. 显式初始化
	// 显式初始化数组
	var arr1 = [5]int{1, 2, 3, 4, 5}
	fmt.Println(arr1)
  1. 忽略长度,显式初始化
	// 省略长度,自动计算长度
	var arr2 = [...]int{1, 2, 3, 4, 5}
	fmt.Println(arr2)
  1. 根据索引初始化部分元素值
	// 指定索引初始化
	var arr3 = [...]int{1: 1, 3: 3, 5: 5}
	fmt.Println(arr3)

以上三段代码的执行结果为:
在这里插入图片描述

1.2 访问和修改元素

  • 与C语言等编程语言一致,直接使用下标n - 1访问或修改数组的第n个元素,如arr[4] = 6即表示将数组arr的第5个元素的值修改为6

2 Go切片

切片可以视为为数组开辟的一个访问窗口,其一定是与一个数组绑定的,具有以下特点:

  1. 动态长度:切片的长度可以动态变化。
  2. 引用类型:切片是引用类型,多个切片可以引用同一底层数组,修改一个切片会影响其他引用相同数组的切片。
  3. 底层数组:切片实际上是对底层数组的引用,包含指向数组的指针、长度和容量。

2.1 定义与初始化

2.1.1 定义

  • 定义方式与数组基本一致,只是取消了长度的限制,且必须初始化后才能访问或修改元素
// 定义切片
	var sl []int
	fmt.Println(sl)
	// len 和 cap 为 0
	fmt.Println(len(sl))
	fmt.Println(cap(sl))

在这里插入图片描述

2.1.2 初始化

  • 切片可以直接显式初始化,或者使用make函数、基于数组进行初始化:
    1. 显式初始化
    // 显式初始化
    sl1 := []int{1, 2, 3, 4, 5}
    fmt.Println(sl1)
    fmt.Println(len(sl1))
    fmt.Println(cap(sl1))
    
    1. 使用make函数
    // 使用make函数
    // 指定长度
    sl2 := make([]int, 5)
    fmt.Println(sl2)
    fmt.Println(len(sl2))
    fmt.Println(cap(sl2))
    // 指定长度和容量
    sl3 := make([]int, 5, 10)
    fmt.Println(sl3)
    fmt.Println(len(sl3))
    fmt.Println(cap(sl3))
    
    1. 基于数组
    // 基于数组初始化切片
    arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    sl4 := arr[2:6]
    fmt.Println(sl4)
    fmt.Println(len(sl4))
    fmt.Println(cap(sl4))
    

以上代码运行结果如下:
在这里插入图片描述

2.2 访问与修改元素

与数组一致,使用下标访问或修改元素

2.3 其他操作

2.3.1 获取长度和容量

fmt.Println(len(s))  // 获取长度
fmt.Println(cap(s))  // 获取容量

2.3.2 添加元素

s := []int{1, 2, 3}
s = append(s, 4, 5)

2.3.3 分片

// 基于切片创建切片
    full := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Println(full)           // 输出原切片
    fmt.Println(len(full))      // 输出原切片长度
    fmt.Println(cap(full))      // 输出原切片容量
    
    part1 := full[2:8]
    fmt.Println(part1)          // 输出 part1 切片
    fmt.Println(len(part1))     // 输出 part1 长度
    // 切片的容量等于数组容量减去切片初始位置
    fmt.Println(cap(part1))     // 输出 part1 容量(cap = 10 - 2)
    
    part2 := full[2:]
    // 切片进行扩容后,可能会影响原切片,取决于切片的容量
    part2 = append(part2, 11)   // 可能分配新的底层数组
    part2[0] = 0                // 修改 shared 底层数组
    fmt.Println(full)           // 可能被 part2 修改
    fmt.Println(len(full))
    fmt.Println(cap(full))
    fmt.Println(part2)
    fmt.Println(len(part2))
    fmt.Println(cap(part2))
    
    // 原切片进行扩容,也可能影响基于原切片衍生的切片
    part3 := full[:]
    full = append(full, 11)     // 可能分配新的底层数组
    fmt.Println(full)           // 新数组
    fmt.Println(len(full))
    fmt.Println(cap(full))
    fmt.Println(part3)          // 旧数组
    fmt.Println(len(part3))
    fmt.Println(cap(part3))

运行结果:
在这里插入图片描述

2.3.4 拷贝

  • 使用copy()函数拷贝两个切片时,会将源切片的数据逐个拷贝到目的切片指向的数组中,拷贝数量取两个切片长度的最小值,也就是说,拷贝过程中不会发生扩容
sl1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	sl2 := make([]int, 5)
	copy(sl2, sl1)
	fmt.Println(sl2)
	fmt.Printf("len of sl2 : %d\n", len(sl2))
	fmt.Printf("cap of sl2 : %d\n", cap(sl2))

运行结果:
在这里插入图片描述

3 底层实现

3.1 底层结构

在Go语言中,切片的底层结构是一个包含三个字段的数据结构:

  1. 指向底层数组的指针(pointer):指向实际存储数据的数组。
  2. 切片的长度(length):表示切片中元素的数量。
  3. 切片的容量(capacity):表示从切片的起始位置到底层数组末尾的元素数量。
type SliceHeader struct {
    Data uintptr  // 指向底层数组的指针
    Len  int      // 切片的长度
    Cap  int      // 切片的容量
}

3.2 扩容机制

3.2.1 扩容的时机

当切片的元素个数超过底层数组的容量时,就需要进行扩容。切片的容量大小是指其底层数组的大小,而长度是切片中实际存储的元素个数

3.2.2 扩容规则

当切片需要扩容时,Go 语言会按照一定的策略重新分配底层数组,并将原有数据复制到新的数组中。具体扩容的规则如下:

  1. 如果切片的长度(即实际元素数量)小于 1024,那么在扩容时新数组的大小将扩大为原数组的 2 倍。即新容量将是原容量的 2 倍。
  2. 如果切片的长度大于等于 1024,则在扩容时新数组的大小只会增加原数组容量的 25%。也就是说,新容量将是原容量的 1.25 倍。
  3. 如果扩容后的新容量仍然不足以存储新的元素,那么会继续按照上述规则进行扩容,直到新容量足够大

3.2.3 扩容的影响

切片的扩容会导致底层数组的重新分配和数据的拷贝,因此扩容的成本是相对较高的。在大量操作切片的场景中,尽量提前估计所需容量,以减少不必要的扩容次数,从而提高性能

3.2.4 示例

	// 创建初始长度为 5,容量为 5 的切片
    s := make([]int, 5)
 	fmt.Println("Length:", len(s), "Capacity:", cap(s)) // 输出 Length: 5 Capacity: 5
    
    // 追加元素,此时长度为 6,超过了初始容量,触发扩容
    s = append(s, 6)
    fmt.Println("Length:", len(s), "Capacity:", cap(s)) // 输出 Length: 6 Capacity: 10
    
    // 追加元素,继续触发扩容
    for i := 7; i <= 15; i++ {
        s = append(s, i)
        fmt.Println("Length:", len(s), "Capacity:", cap(s))
    }

运行结果:
在这里插入图片描述

  • 38
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Golang数组切片是两种不同的数据类型,用于存储相同数据类型的容器。数组的长度是固定的,而切片的长度是可变的。在日常应用中,切片的使用更为普遍。 数组在声明时需要指定长度,并且在初始化时必须提供相同长度的元素。例如,`a := int{1, 2, 3}`就是一个长度为3的整数数组数组的长度一旦确定后就不能更改。 切片是基于数组的引用类型。它不需要指定固定的长度,并且可以根据需要动态扩展或缩小。切片包装着底层数组,通过指定起始索引和结束索引来指定子集。例如,`b := a[:]`就是一个切片,它包含了数组a的所有元素。 数组适用于需要固定长度的场景,而切片适用于长度可变的情况。在实际应用中,切片更常用,因为它提供了更大的灵活性和便利性。 总结: - 数组是长度固定的容器,切片是长度可变的容器; - 数组在声明时需要指定长度,切片则不需要; - 数组的长度一旦确定后就不能更改,而切片可以根据需要动态扩展或缩小; - 切片是基于数组的引用类型,可以通过指定起始索引和结束索引来指定子集。 参考资料: Golang中的「数组」和「切片」都是存储同一数据类型的容器,只不过Golang中的数组长度是固定的,而切片的长度是可变化的。 切片是引用类型,切片包装的数组称为该切片的底层数组。我们来看一段代码://a是一个数组,注意数组是一个固定长度的,初始化时候必须要指定长度,不指定长度的话就是切片了 a := int{1, 2, 3} //b是数组,是a...。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值