Go语言中的数组和切片 len cap append copy

1,837 阅读3分钟

Go语言中的数组和切片 len cap append copy

先来看看Golang中的数组

其实在循环那一节用到过数组,我快速介绍一下。

  • 数组中是固定长度的连续空间(内存区域)
  • 数组中所有元素的类型是一样的
 var a1 [10]int
  
 //初始化数组
 var b1 = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

多维数组

//声明二维数组,只要 任意加中括号,可以声明更多维,相应占用空间指数上指
 var arr [3][3]int
 //赋值
 arr = [3][3]int{
  {1, 2, 3},
  {2, 3, 4},
  {3, 4, 5},
 }

何谓切片?

类比c语言,一个int型数组int a[10],a的类型是int*,也就是整型指针,而c语言中可以使用malloc()动态的分配一段内存区域,c++中可以用new()函数。例如:

int* a = (int *)malloc(10);
int* b = new int(4);

此时,ab的类型也是int*ab此时分配内存的方式类似于go语言中的切片。

Go的数组和切片都是从c语言中延续过来的设计。

有何不同?

var sliceTmp []int

可以看到和c不同的是,go可以声明一个空切片(默认值为nil),然后再增加值的过程中动态的改变切片值大小。

怎么动态增加?

增加的方式只有一种,使用append函数追加。

sliceTmp = append(sliceTmp, 4)
sliceTmp = append(sliceTmp, 5)

每个切片有长度len和容量cap两个概念,长度是我们最熟知的,和数组长度相同,可以直接用来遍历。

for _,v := range slice1{
  fmt.Println(v)
 }

用切糕来对比

每个切片,在声明或扩建时会分配一段连续的空间,称为容量cap,是不可见的;真正在使用的只有一部分连续的空间,称为长度len,是可见的。

每次append时,如果发现cap已经不足以给len使用,就会重新分配原cap两倍的容量,把原切片里已有内容全部迁移过去。

新分配的空间也是连续的,不过不一定直接在原切片内存地址处扩容,也有可能是新的内存地址。

切片的长度与容量,len cap append copy

slice1 := []int{1, 2, 3}

普通切片的声明方式,长度和容量是一致的。

len=3 cap=3 slice=[1 2 3]

当然,控制权在我们手上,我们可以自己控制长度和容量,

 slice1 = make([]int, 3, 5) // 3 是长度 5 是容量

输出

len=3 cap=5 slice=[0 0 0]

尝试使用一般的方式扩容

slice1[len(slice1)] = 4 
//报错 panic: runtime error:
//index out of range [3] with length 3

这种方式是会报错的,虽然容量是 5 ,但是数组长度是3,这里是以长度为准,而不是容量,append内部如果超过容量相当于创建了一个新数组,每个新数组都是定长的,只不过外部是切片。

尝试扩容

slice1 = append(slice1, 4)

输出,可以发现len扩容了!

len=4 cap=5 slice=[0 0 0 4]

让我们连续扩容,让容量超过5

slice1 = append(slice1, 5)
slice1 = append(slice1, 6) // 到这里长度超过了容量,容量自动翻倍为 5*2

输出

len=6 cap=10 slice=[0 0 0 4 5 6]

上面的过程,我 用自己的代码模拟一遍

// 上面容量自动翻倍的过程可以看作和下面一致
 slice1 = make([]int, 3, 5) // 3 是长度 5 是容量
 slice1 = append(slice1, 4)
 slice1 = append(slice1, 5)

// 长度不变,容量自动翻倍为 5*2
 slice2 := make([]int, len(slice1), 
          (cap(slice1))*2)

 // 拷贝 slice1 的内容到 slice2 
  // 注意是后面的拷贝给前面
 copy(slice2, slice1) 
  
 slice2 = append(slice2, 6) 

所以,你理解容量,长度的概念了吗?