该片文章我将详细带领大家搞定这道面试必考题。
开始之前
在众多常用的高级语言中比如:C#,Java,JavaScript等都有数组的概念。
如果你是以上语言的使用者,当被问及数组是什么的时候,第一反应就是数组是引用类型,指向的是一块内存的首地址。 其实这个回答没有什么问题,但是在Go中却不对,Go是什么?我觉得应该是C的语法糖。
谈谈数组
我的理解:一堆相同类型的数据,占用固定大小的内存,按照固定的顺序紧密排列在一起。
抛开语言层面对数组定义的干扰,数组本身就应该是上面的概念
再谈值类型与引用类型
值类型: 当值类型数据需要被传递时,传递的总是内存中的数据的副本
引用类型:引用类型需要被传递时,传递的总是对应数据的首地址
面试官想听到的数组是什么样子的
从下面三个方向回答(这里就不考虑内存回收策略了,太复杂),不太较真的面试官应该会给满分。
-
数组申请内存时需要先清空对应内存的数据
数组在被创建时,会将对应的内存空间全部初始化为0
-
数组是固定大小的,用完时无法容纳更多数据
在数组被创建的时候,通常必须指定数组元素的个数,个数*每个元素的大小=数组的总大小。 所以在创建数组的时候,其实已经占用了对应大小的内存,不管你放不放真实数据,内存已经被消耗了。
-
数组要保存容量之外的数据,需要使用更大的数组(或者说是数组扩容,大家都是这么回答,但是我觉得不太准确)
通常,当数组放不下更多数据时,几乎所有的语言扩容策略都是将原来数组的大小*2,重新申请一块内存,并将原来数组中的数据复制过来。 由此,我们可以看到数组的扩容极其麻烦,如果数组本身就很大,那扩容将是一场灾难。
Go中的数组
在Go语言中定义了数组是值类型,为什么是值类型?
package main
import (
"fmt"
)
func main() {
a:=[10]int8{}
b:=a
fmt.Printf("a:%p b:%p",&a,&b)
}
# out
a:0xc000102070 b:0xc000102080
a,b不同的内存地址,说明了b数组开辟了新的内存空间,a将数据复制到了b对应的内存中
Go中的切片
在Go语言中切片是数组的引用,指向的是一个数组的地址。这句话怎么解释?
package main
import (
"fmt"
"reflect"
)
func main() {
a:=[10]int8{}
b:=a[1:]
fmt.Printf("a:%s b:%s\n", reflect.TypeOf(a),reflect.TypeOf(b))
fmt.Printf("a:%p a[1]:%p b:%p b len:%d b cap:%d\n",&a,&(a[1]),&b,len(b),cap(b))
}
# out
a:[10]int8 b:[]int8
a:0xc0000120a0 a[1]:0xc0000120a1 b:0xc000004078 b len:9 b cap:9
从上面的例子可以看到,a是一个数组,b是一个切片,但b并不指向a[1]元素的地址,这样就说明了,在创建b时,首先创建了一个大小为9个元素的新数组,将a的第1个到最后一个元素复制到了b中。
package main
import "log"
func main() {
arr1:=[4]int{1,2,3,4}
arr := []int{1, 2, 3, 4}
changeArr1(arr1)
log.Println(arr1)
changeArr(arr1[:])
log.Println(arr1)
changeArr(arr)
log.Println(arr)
}
func changeArr1(arr [4]int){
for i := 0; i < len(arr); i++ {
arr[i] = 0
}
}
func changeArr(arr []int) {
for i := 0; i < len(arr); i++ {
arr[i] = 0
}
}
# out
2021/03/26 23:16:40 [1 2 3 4]
2021/03/26 23:16:40 [0 0 0 0]
2021/03/26 23:16:40 [0 0 0 0]
从这个例子可以看出arr1的参数按值类型传递,arr的参数是按引用传递的(其实不是按引用传递,还是按值传递的,为什么?接着往下看)。
切片虽然好用,但要慎用
在Go中最常用的莫过于切片,这都归功于切片提供的一系列使用api,这里我就不解释这些api了,网上教程一大把,自选吧。
这里我们主要解释以下为什么要慎用切片,我觉得有以下两个问题需要注意:
1. 切片扩容
上面我们已经解释了数组的扩容,也知道了,切片其实指向的是一个数组的首个元素的内存地址,除此之外切片还有一个len与cap字段分别表示当前的元素个数以及最大能容纳的元素个数,扩容其实就是数组扩容,只不过扩容后切片的指针会指向新的数组的首个元素地址。
arr:=make([]int,10,20)
log.Println(len(arr))
log.Printlncap(arr)
# out
10
20
2. 切片的传递
由于切片扩容后,会导致指向的地址发生变化,所以我们应尽可能的将切片的使用限制在函数、代码段中,避免将切片的传递。
注意一: 切片指向的数组不需要扩容时
package main
import "log"
func main() {
arr := make([]int, 5, 10)
log.Println(arr)
log.Printf("arr的地址:%p",&arr
// 将arr作为参数传入change中
change(arr)
log.Printf("arr的地址:%p",&arr)
log.Println(arr)
}
// 改变传入参数中元素的值,但不改变参数的元素个数
func change(arr1 []int) {
arr1[0] = 1
arr1[1] = 2
log.Printf("arr1的地址:%p", &arr1)
}
# out
2021/03/30 00:06:53 [0 0 0 0 0]
2021/03/30 00:06:53 arr的地址:0xc000004078
2021/03/30 00:06:53 arr1的地址:0xc0000040f0
2021/03/30 00:06:53 arr的地址:0xc000004078
2021/03/30 00:06:53 [1 2 0 0 0]
上面代码中,arr与arr1虽然指向了不同的地址,但是change函数改变了传入参数arr1的值,导致了arr的值也发生了变化,这也说明arr,arr1其实指向的还是一个地址,切片数据类型内有一个字段用来保存数组的地址,所以在数组不需要扩容的时,导致指向的是一个地址。
注意二:当切片指向的数组需要扩容时
package main
import "log"
func main() {
arr := make([]int, 5, 10)
log.Println(arr)
log.Printf("arr的地址:%p",&arr)
change(arr)
log.Printf("arr的地址:%p",&arr)
log.Println(arr)
}
func change(arr1 []int) {
arr1[0]=111
arr1 = append(arr1, 1,2,3,4,5,6)
log.Println("arr1:",arr1)
log.Printf("arr1的地址:%p", &arr1)
}
# out
2021/03/30 00:18:15 [0 0 0 0 0]
2021/03/30 00:18:15 arr的地址:0xc000004078
2021/03/30 00:18:15 arr1: [111 0 0 0 0 1 2 3 4 5 6]
2021/03/30 00:18:15 arr1的地址:0xc0000040f0
2021/03/30 00:18:15 arr的地址:0xc000004078
2021/03/30 00:18:15 [111 0 0 0 0]
从上面代码我们可以看到,当arr1修改arr1[0]时影响了arr,但后续的append与arr1[2]却没有影响arr,这也就说明了,其实在扩容前,aar与arr1中的数组地址是一样的,但是arr1扩容后,指向的数组地址发生了变化,所以无法影响arr中的值。
总结
仅针对Go语言的实现
-
数组和切片都是值类型
-
数组是固定大小的,不可扩容
-
切片是不固定大小的,可以扩容,切片内其实还是包涵了一个数组
-
切片被传递时,如果不改变容量大小时,指向的数组地址传递前后一致,否则,改变大小的切片会指向新的数组
-
使用切片前要明确提前预知被传递的切片会发生什么样的变化
-
尽量将切片变量作为全局变量使用,当切片在代码断中使用时,尽量避免被传递