美文网首页
go 数据结构 -- array&slice

go 数据结构 -- array&slice

作者: 朱建涛 | 来源:发表于2021-03-24 22:36 被阅读0次

    数据类型的本质:固定内存大小的别名。

    数据类型的作用:编译器预算对象或变量分配内存空间的大小。

    数组 array

    数组是同一种数据类型的固定长度的序列,指向一段连续的内存空间。

    声明

    arr := [6]int{1,2,3,4,5,6}
    

    特性

    • 因为数组的内存分布是连续的,所以数组访问任一元素的效率很高,O(1)。
    • 元素类型和数组长度都是数组类型的一部分,不同长度的数组是不同的类型。
    • 数组是值类型,改变其副本的值不会改变原数组的值。

    数组指针和指针数组

    数组指针

    即声明一个指针变量,指向一个数组:

    arr := [6]int{5:9}  //第6个元素为9
    var ptr *[6]int = &arr
    

    需要注意的是,指针变量ptr的类型是*[6]int,也就是说它只能指向包含6个元素的整型数组,否则编译报错。

    指针数组

    即数组的元素类型是指针。

    x,y := 1,2
    var arrPtr = [5]*int{1:&x,3:&y}
    

    函数间传递数组

    如果实参是一个非常大的数组,因函数参数只有值传递,即需要重新拷贝变量,拷贝导致的内存和性能开销较大。可将数组指针作为参数传递,拷贝开销小,但也需要考虑到函数内数组指针修改会影响原数组。

    切片 slice

    切片与数组类似,存放相同数据类型的元素,不同的是切片基于底层数组,可按需扩缩容。切片是底层数组的一个视图,也可以说切片是对数组的抽象。

    切片的内部实现中有三个变量,指针 ptr,长度 len 和容量 cap。

    // runtime/slice.go
    type slice struct {
        array unsafe.Pointer // 指针,数据存储在底层数组中,而指针指向可以通过切片访问到的第一个元素。
        len   int            // 长度,我们只能访问切片长度范围内的元素。
        cap   int            // 容量,表示可以扩展的最大大小。容量必须大于等于长度。
    }
    
    图片

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

    声明与初始化

    make 函数创建

    s1 := make([]int,3,5)   // 创建一个可用的空切片,长度为3,容量为5,容量也可以不传
    fmt.Println(s1)         // [0 0 0]
    

    字面量创建

    s2 := []int{1,2,3,4,5}  // 创建长度和容量都是5的整型切片
    

    截取已有的数组或者切片创建

    a := []int{1,2,3,4,5}
    t := a[3:4]
    

    截取得到的切片,和原切片或原数组共享底层数组,但是两者能访问到底层数组的范围是不同的。

    截取获得的新切片的长度和容量计算

    对容量为 k 的切片执行[i,j]操作之后,获得的新切片的长度和容量是j-ik-i

    对容量为 k 的切片执行[i,j,l]操作之后,获得的新切片的长度和容量是j-il-i,其中第三个变量 l 用于限定新切片的容量,j 和 l 必须在原数组或者原 slice 的容量范围内(小于等于 k)。

    新切片与原数组或者切片的关系

    截取得到的新切片和原数组或原切片是基于同一个底层数组的,所以当修改的时候,底层数组的值就会被改变,原切片的值也随之改变了。

    可以把截取得到的新切片看做是原数组或原切片的一个视图。

    图片

    但如果因为执行 append 操作使得新 slice 底层数组扩容,移动到了新的位置,两者就不会相互影响了。所以,问题的关键在于两者是否会共用底层数组

    nil 切片和空切片

    nil 切片

    var s []int                 // 声明了一个 nil 切片
    fmt.Println(s == nil)       // 输出 true
    fmt.Println(len(s), cap(s)) // 输出:0 0
    s = append(s, 1)            // 使用 append 函数可以为 nil 切片增加元素
    

    切片的零值是 nil。因为切片是底层数组的引用,nil 切片指向底层数组的指针为 nil,即不指向任何底层数组。

    空切片

    s := make([]int, 0)         // 1、使用 make 创建空的整型切片
    s := []int{}                // 2、使用切片字面量创建空的整型切片
    fmt.Println(s == nil)       // 输出 false
    fmt.Println(s)              // 输出:[]
    fmt.Println(len(s), cap(s)) // 输出:0 0
    

    与 nil 切片一样,空切片的长度和容量也都是 0,说明切片底层的数组大小为 0,是一个空数组(没有分配任何的存储空间)。

    不管是使用 nil 切片还是空切片,对其调用内置函数 append、len 和 cap 的效果都是一样的。官方建议尽量使用 nil 切片。

    nil slice 可以直接 append ,因为 append 最终都是调用 mallocgc 来向 Go 的内存管理器申请到一块内存,然后再赋给原来的 nil slice 或 empty slice。

    增长/扩容

    使用内建函数 append 能够帮我们处理切片增长的一些细节。

    • append 可往切片追加一个或多个值,然后返回一个新的切片。应注意 Go 编译器不允许调用了 append 函数后不使用返回值。
    • append 函数会使新的切片长度增加。
    • append 函数实际上是往底层数组添加元素。
    • 新切片容量是否增长取决于原切片剩余容量和需要追加的元素数目。当剩余容量不足时,append 函数会创建一个新的数组并将原数组元素拷贝到新数组中,再追加新的值。append 函数会智能地增加底层数组的容量,目前的算法是:当数组容量小于等于1024时,会成倍地增加;当超过1024,增长因子变为1.25,也就是说每次会增加25%的容量(这个说法是错误的,还有内存对齐的操作)。

    要注意的是,通过截取创建的切片,如果切片剩余容量能存下 append 追加的元素,切片长度增长而不扩容,追加的元素会改变原切片或数组的值;如果不能存下追加的元素,切片长度增长并进行扩容,扩容操作为 append 函数创建一个新的底层数组,将原数组的值复制到新数组里,再追加新的值,因为新切片与原切片或数组的底层数组不再相同,追加的元素不会改变原切片或数组的值,且后续两个切片不再相互影响。关键在于两者是否会共用底层数组

    一般我们在创建新切片的时候,最好要让新切片的长度和容量一样,这样我们在追加操作的时候就会生成新的底层数组,和原有数组分离,就不会因为共用底层数组而引起奇怪问题。

    copy 函数

    Go 提供了内置函数 copy,可以将一个切片复制到另一个切片。

    func copy(dst, src []Type) int  // 函数返回两者长度的最小值
    

    如果 dst 切片为 nil 切片,copy 之后,dst 切片仍为 nil。而 nil 切片 append 非空切片后,变为非空切片。

    copy 只会复制,不会追加。

    函数间传递切片

    切片在函数间以值的方式传递。当直接用切片作为函数参数时,可以改变切片的元素,不能改变切片本身;想要改变切片本身,可以将改变后的切片返回,函数调用者接收改变后的切片或者将切片指针作为函数参数。

    由于切片的尺寸很小(在 64 位架构的机器上,一个切片需要 24 字节的内存:指针字段、长度和容量字段各需要 8 字节),在函数间复制和传递切片成本很低。

    删除切片中的元素

    Go 没有提供删除切片元素的函数,可以通过截取和 append 函数实现。

    s := []int{1, 2, 3, 4, 5, 6}
    s = append(s[:2], s[3:]...)    // 删除索引为2的元素
    

    其他骚操作

    切片垃圾回收

    巨型 slice 产生的垃圾回收问题

    相关文章

      网友评论

          本文标题:go 数据结构 -- array&slice

          本文链接:https://www.haomeiwen.com/subject/soekhltx.html