当前位置:   article > 正文

Go 程序员面试笔试宝典-数组与切片

2023 数组和切片的区别

数组与切片有什么区别

slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。

数组是定长的,长度定义好之后,不能再更改。在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型

而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。

数组就是一片连续的内存, slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组

  1. // runtime/slice.go
  2. type slice struct {
  3.     array unsafe.Pointer // 元素指针
  4.     len   int // 长度 
  5.     cap   int // 容量
  6. }

slice 的数据结构如下:bdc208e38432a15b5a0313730ea1a389.png

注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。

【引申1】 [3]int 和 [4]int 是同一个类型吗?

不是。因为数组的长度是类型的一部分,这是与 slice 不同的一点。

【引申2】 下面的代码输出是什么?

说明:例子来自雨痕大佬《Go学习笔记》第四版,P43页。这里我会进行扩展,并会作图详细分析。

  1. package main
  2. import "fmt"
  3. func main() {
  4.     slice := []int{0123456789}
  5.     s1 := slice[2:5]
  6.     s2 := s1[2:6:7]
  7.     s2 = append(s2, 100)
  8.     s2 = append(s2, 200)
  9.     s1[2] = 20
  10.     fmt.Println(s1)
  11.     fmt.Println(s2)
  12.     fmt.Println(slice)
  13. }

结果:

  1. [2 3 20]
  2. [4 5 6 7 100 200]
  3. [0 1 2 3 20 5 6 7 100 9]

s1 从 slice 索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。s2 从 s1 的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。e473634c39b3e8451849368b342e16ce.png

接着,向 s2 尾部追加一个元素 100:

s2 = append(s2, 100)

s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。dbaa2aaf1984ce9482c9fc2dfa9f3fc3.png

再次向 s2 追加元素200:

s2 = append(s2, 100)

这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。d918996fe89431c1b8ee59cafbb7ce47.png

最后,修改 s1 索引为2位置的元素:

s1[2] = 20

这次只会影响原始数组相应位置的元素。它影响不到 s2 了,人家已经远走高飞了。4f4ee903b098ad7a8978951427f24ff8.png

再提一点,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。

切片作为函数参数

slice 其实是一个结构体,包含了三个成员:len, cap, array。分别表示切片长度,容量,底层数据的地址

当 slice 作为函数参数时,就是一个普通的结构体。其实很好理解:若直接传 slice,在调用者看来,实参 slice 并不会被函数中的操作改变;若传的是 slice 的指针,在调用者看来,是会被改变原 slice 的。

值得注意的是,不管传的是 slice 还是 slice 指针,如果改变了 slice 底层数组的数据,会反应到实参 slice 的底层数据。为什么能改变底层数组的数据?很好理解:底层数据在 slice 结构体里是一个指针,尽管 slice 结构体自身不会被改变,也就是说底层数据地址不会被改变。但是通过指向底层数据的指针,可以改变切片的底层数据,没有问题。

通过 slice 的 array 字段就可以拿到数组的地址。在代码里,是直接通过类似 s[i]=10 这种操作改变 slice 底层数组元素值。

另外,值得注意的是,Go 语言的函数参数传递,只有值传递,没有引用传递。

来看一个代码片段:

  1. package main
  2. func main() {
  3.     s := []int{111}
  4.     f(s)
  5.     fmt.Println(s)
  6. }
  7. func f(s []int) {
  8.     // i只是一个副本,不能改变s中元素的值
  9.     /*for _, i := range s {
  10.         i++
  11.     }
  12.     */
  13.     for i := range s {
  14.         s[i] += 1
  15.     }
  16. }

运行一下,程序输出:

[2 2 2]

果真改变了原始 slice 的底层数据。这里传递的是一个 slice 的副本,在 f 函数中,s 只是 main 函数中 s 的一个拷贝。在f 函数内部,对 s 的作用并不会改变外层 main 函数的 s。面试官:说下Golang Slice的底层实现,泪崩了!

要想真的改变外层 slice,只有将返回的新的 slice 赋值到原始 slice,或者向函数传递一个指向 slice 的指针。我们再来看一个例子:

  1. package main
  2. import "fmt"
  3. func myAppend(s []int) []int {
  4.     // 这里 s 虽然改变了,但并不会影响外层函数的 s    //来源公众号:码农编程进阶笔记    s = append(s, 100)
  5.     return s
  6. }
  7. func myAppendPtr(s *[]int) {
  8.     // 会改变外层 s 本身
  9.     *s = append(*s, 100)
  10.     return
  11. }
  12. func main() {
  13.     s := []int{111}
  14.     newS := myAppend(s)
  15.     fmt.Println(s)
  16.     fmt.Println(newS)
  17.     s = newS
  18.     myAppendPtr(&s)
  19.     fmt.Println(s)
  20. }

运行一下,程序输出:

  1. [1 1 1]
  2. [1 1 1 100]
  3. [1 1 1 100 100]

myAppend 函数里,虽然改变了 s,但它只是一个值传递,并不会影响外层的 s,因此第一行打印出来的结果仍然是 [1 1 1]。

而 newS 是一个新的 slice,它是基于 s 得到的。因此它打印的是追加了一个 100 之后的结果:[1 1 1 100]。

最后,将 newS 赋值给了 s,s 这时才真正变成了一个新的slice。之后,再给 myAppendPtr 函数传入一个 s 指针,这回它真的被改变了:[1 1 1 100 100]。

~~~~~推荐阅读~~~~~

2023年PHP/Go面试题集总结【建议收藏】

Golang 常⻅⾯试题⽬解析(一)

[面试] Golang 面试题

2023年golang最新面试题来咯

【建议收藏】golang 常用的标准库和管理命令

5bc41035fba058e7dea255e390bfaf7f.jpeg

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/383969
推荐阅读
相关标签
  

闽ICP备14008679号