go语言学习笔记(七)——数组和切片

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_32023305/article/details/82252537
  • 前言

Go语言的数组(array)类型和切片(slice)类型都可以用来存储某一种类型的值(元素),不过他们最重要的不同是:数组类型的值的长度是固定的,而切片类型的值是可变长的。数组的长度必须再声明时就给定,并且在之后不会再改变,而切片声明时只有其元素的类型,没有其长度。可以把切片看作是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。后者被叫做前者的底层数组,前者被看作是对后者的某个连续片段的引用。切片的长度可以自动地随着其中元素数量的增长而增长,但不会随着元素数量的减少而减少。通过调用内建函数len得到切片长度,调用内建函数cap,得到切片容量。数组的容量永远等于其长度,都是不可变的,而切片却不是这样,由此抛出下面问题

  • 怎样正确估算切片的长度和容量

先看如下示例

// demo15.go
package main
 
import (
	"fmt"
)
 
func main() {
	s1:=make([]int, 5)// 指明长度
	fmt.Printf("The value of s1 is: %v\n", s1)
	fmt.Printf("The length of s1 is: %d\n", len(s1))
	fmt.Printf("The capacity of s1 is: %d\n", cap(s1))
	fmt.Printf("The value of s1 is: %d\n", s1)
	s2:=make([]int, 5, 8)// 指明长度和容量
	fmt.Printf("The length of s2:%d\n", len(s2))
	fmt.Printf("The capacity of s2 is:%d\n", cap(s2))
	fmt.Printf("The value of s2 is:%d\n", s2)
}

上述程序先用内建函数make声明了一个[]int类型的变量s1,指明了该切片的长度为5,接着用同样方式声明了切片s2,只不过多指明了一个参数8作为该切片的容量。那么切片是s1和s2的容量都是多少?

运行后结果为

The value of s1 is: [0 0 0 0 0]
The length of s1 is: 5
The capacity of s1 is: 5
The value of s1 is: [0 0 0 0 0]
The length of s2:5
The capacity of s2 is:8
The value of s2 is:[0 0 0 0 0]
  • 问题解析

s1的容量为什么是5呢?因为在声明s1时把长度设置成了5。当我们用make函数初始化切片时,如果不指明其容量,那么它就会和长度一致,如果在初始化的时候指明了容量,那么切片的容量就是它了,所以s2的容量为8。这种情况下,它的容量实际代表了它的底层数组的长度

注意:切片的底层数组长度是不可变的。

想象有一个窗口,通过这个窗口可以看到一个数组,但是不一定能看到该数组中所有元素,有一部分是被挡住了,有时候只能看到连续的一部分元素。

该例中,这个数组就是切片s2的底层数组,而这个窗口就是s2本身。s2的长度就是这个窗口的宽度,决定了你透过s2可以看到其底层数组中的哪几个连续的元素。

由于s2的长度是5(定义的),所以你可以看到其底层数组中的第1到第5个元素,对应的底层数组的索引范围是0到4([0,4])。

切片代表的窗口被划分成一个个的小格子,每个小格子都对应着其底层数组中的某一个元素。

s2中,窗口最左边的小格子对应的正好是其底层数组中的第一个元素,即索引为0的那个元素。因此,s2中的索引从0到4的元素,就是其底层数组中索引从0到4的那5个元素。

长度

当我们用make函数或切片值字面量(如:[]int{1,2,3})初始化一个切片时,该窗口最左边的那个小格子总是会对应其底层数组中的第1个元素。

但是当我们通过“切片表达式”基于某个数组或者切片生成新切片的时候,情况就复杂一点,如:

 
  1. s3 := []int{1,2,3,4,5,6,7,8}

  2. s4 := s3[3:6]

切片s3中有8个元素(整数1到8),s3的长度和容量都是8.

s4从s3中通过切片表达式初始化而来,其中[3:6]表达的就是透过新窗口能看到的,用减法可以计算出长度,6减去3为3.而索引范围为从3到5(不包括6,可以引申为区间表示法,即[3,6))。

s4中的索引从0到2,指向的元素对应的是s3中索引从3(起始索引)到5(结束索引)的3(长度)个元素。

容量

切片的容量代表了它的底层数组的长度,但这仅限于用make函数或者切片字面量([]int{1,2,3})初始化切片的情况。

一个切片的容量可以被看作是透过这个窗口最多可以看到的底层数组中元素的个数。

s4是通过在s3上施加切片操作得来的,所以s3的底层数组等于s4的底层数组;而且,在底层数组不变的情况下,切片代表的窗口可以向右扩展,直至其底层数组的末尾,所以s4的容量就是其底层数组的长度8减去上述切片表达式([3:6])中的起始索引3,等于5。

要注意的是,切片代表的窗口是无法向左扩展的,也即无法透过s4看到s3中最左边的那3个元素。

通过切片表达式:

s4[0:cap(s4)]

可以把切片的窗口向右扩展到最大,该表达式会产生一个新的切片[]int{4,5,6,7,8},长度和容量都是5.

知识扩展

详细介绍切片的长度和容量计算。

问题1:怎样估算切片容量的增长

如果一个已定义的切片无法容纳更多的元素,Go就会自动为其扩容。但是,扩容不会改变原来的切片,而是会生成一个容量更大的切片,然后把原切片元素和新元素都复制到新切片中。

对于扩容的计算方法,有两种:

1、一般情况下,新容量是原容量的2倍;

2、当原长度>=(大于或等于)1024时,新容量基准不再是2倍,而是1.25倍。新容量基准会在此基础上不断地被调整,直到不小于原长度与要追加的元素数量之和(新长度),最终,新容量往往会比新长度大一些,也有可能相等。

特殊情况:如果一次追加的元素过多,以至于使得新长度比原容量的2倍还要大,那么新容量就会以新长度为基准,最终的新容量往往比新容量基准要更大一些。

问题2:切片的底层数组什么时候会被替换

注意:一个切片的底层数组永远不会被替换。

因为在“扩容”的时候,也一定会生成新的底层数组,同时也生成新的切片,而对原切片及其底层数组没有做任何改动。

append函数使用注意:在无需扩容时,返回的是指向原底层数组的切片;需要扩容时,返回的是指向新底层数组的新切片。

只要新长度不会超过切片的原容量,那么使用append函数对其追加元素的时候就不会引起扩容,这只会使得紧邻切片窗口(抽象)右边的元素被新的元素替换掉。

思考题

关于数组和切片,其实还有很多细节需要去关注,这里提出两个思考题:

1、如果有多个切片指向了同一个底层数组,应该注意什么?

答:当两个长度不一的切片使用同一个底层数组,并且两切片的长度均小于数组的容量时,对其中长度较小的一个切片进行append操作,但不超过底层数组容量,这时会影响长度较长切片中原来比较小切片多看到的值,因为底层数组被修改了。

2、怎样沿用“扩容”的思想来对切片进行“缩容”?用代码实践下。

答:生成新的slice

func main() {
s1 := []int{1,2,3,4,5}
printSlice("s1", s1)

s1 = shrinkSlice(s1)

printSlice("s1", s1)
}

func shrinkSlice(x []int) []int{
if( cap(x) > 0 ) {
x = x[0:cap(x)-1]
}
return x
}

func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d %v\n",
s, len(x), cap(x), x)
}

输出结果
s1 len=5 cap=5 [1 2 3 4 5]
s1 len=4 cap=5 [1 2 3 4]

猜你喜欢

转载自blog.csdn.net/sinat_32023305/article/details/82252537