美文网首页
go切片与数组的关系—修改切片导致数组被修改的问题

go切片与数组的关系—修改切片导致数组被修改的问题

作者: 猫尾草 | 来源:发表于2020-09-15 09:09 被阅读0次

    2020-10-29更新
      "切片是指向数组的指针"这句话是不对的。切片就是切片,有自己的属性和方法,只是借用了数组来存储实际的数据。
    切片的数据结构大概为

    type slice struct {
        point int
        len int
        cap int
    }
    

    即,管理一个数组上以point指向元素的数组下标为起点,加上len长度为终点,最大扩容到cap长度为终点的一段数据。
    举例:

    func main() {
        D := [10]int{0,1,2,3,4,5,6}
        A := D[0:3:3]
        B := D[2:5:9]
        C := D[4:7]
    
        fmt.Printf("A: %v\n", A)
        fmt.Printf("B: %v\n", B)
      fmt.Printf("C: %v\n", C)
      fmt.Printf("D: %v\n", D)
    }
    

    注意:

      1. 数组没有指定值的位置会用默认值填充,此处即为0
      1. 创建切片时第三个参数表示切片容量到数组的哪个索引位置,没有就默认到数组末尾,这个下面有用处,所以分三种设置做对比
      1. 从数组取切片时是前闭后开区间,与通常规定操作一致。

    运行输出:

    A: [0 1 2]
    B: [2 3 4]
    C: [4 5 6]
    D: [0 1 2 3 4 5 6 0 0 0]
    

    修改切片内容如下:

    func main() {
        D := [10]int{0,1,2,3,4,5,6}
        A := D[0:3]
        B := D[2:5]
        C := D[4:7]
        A[2] = 999
        B[2] = 666
    
        fmt.Printf("A: %v\n", A)
        fmt.Printf("B: %v\n", B)
        fmt.Printf("C: %v\n", C)
        fmt.Printf("D: %v\n", D)
    }
    

    运行输出:

    A: [0 1 999]
    B: [999 3 666]
    C: [666 5 6]
    D: [0 1 999 3 666 5 6 0 0 0]
    

    可以看出所有修改都同步了,并且体现在数组D上。
    再来看Append操作。Append是扩展切片的长度,但是如果长度超过了预设的容量,就需要换一个底层数组。看下面的程序:

    func main() {
        D := [10]int{0,1,2,3,4,5,6}
        A := D[0:3:3]
        B := D[2:5:7]
        C := D[4:7]
        A = append(A, 333)
        B = append(B, 666)
        C = append(C, 999)
    
        fmt.Printf("A: %v\n", A)
        fmt.Printf("B: %v\n", B)
        fmt.Printf("C: %v\n", C)
        fmt.Printf("D: %v\n", D)
    }
    

    运行输出:

    A: [0 1 2 333]
    B: [2 3 4 666]
    C: [4 666 6 999]
    D: [0 1 2 3 4 666 6 999 0 0]
    

    可以看到A因为预设了[0:3:3]的原因,容量只有3,当前已满,再增加一个333,就切换了新的数组,所以A的修改只体现在自身,对B、数组D都没有影响。
    而B的容量为7,C的容量为5,都有空间,所以修改体现在了数组D上。
    将切片A扩展到容量4,但是增加两个元素:

    func main() {
        D := [10]int{0,1,2,3,4,5,6}
        A := D[0:3:4]
        B := D[2:5:7]
        C := D[4:7]
        A = append(A, 333)
        A = append(A, 332)
        B = append(B, 666)
        C = append(C, 999)
        fmt.Printf("A: %v\n", A)
        fmt.Printf("B: %v\n", B)
        fmt.Printf("C: %v\n", C)
        fmt.Printf("D: %v\n", D)
    }
    

    运行输出:

    A: [0 1 2 333 332]
    B: [2 333 4 666]
    C: [4 666 6 999]
    D: [0 1 2 333 4 666 6 999 0 0]
    

    可以看到添加333时还没有超出切片A的容量,所以333还在数组D上做修改,而添加332时已经超出了A的容量,A换了一个新的数组(现有数据0、1、2、333复制过去),并且在新数组添加332,而不影响原来的数组D。
    为什么强调不要把切片理解为数组的指针呢?这里还有一个非常重要的问题。看代码:

    func main() {
        slice := make([]int, 2, 3)
        for i := 0; i < len(slice); i++ {
            slice[i] = i
        }
        fmt.Printf("slice: %v, addr: %p \n", slice, slice)
        changeSlice(slice)
        fmt.Printf("slice: %v, addr: %p \n", slice, slice)
    }
    func changeSlice(s []int){
        s = append(s, 3)
        s = append(s, 4)
        s[1] = 111
        fmt.Printf("func s: %v, addr: %p \n", s, s)
    }
    

    运行输出:

    slice: [0 1], addr: 0xc0000a0140 
    func s: [0 111 3 4], addr: 0xc0000ca030 
    slice: [0 1], addr: 0xc0000a0140
    

    把changeSlice的s[1] = 111操作提前:

    func main() {
        slice := make([]int, 2, 3)
        for i := 0; i < len(slice); i++ {
            slice[i] = i
        }
        fmt.Printf("slice: %v, addr: %p \n", slice, slice)
        changeSlice(slice)
        fmt.Printf("slice: %v, addr: %p \n", slice, slice)
    }
    func changeSlice(s []int){
      s[1] = 111
        s = append(s, 3)
        s = append(s, 4)
        fmt.Printf("func s: %v, addr: %p \n", s, s)
    }
    

    运行输出:

    slice: [0 1], addr: 0xc0000a0140 
    func s: [0 111 3 4], addr: 0xc0000ca030 
    slice: [0 111], addr: 0xc0000a0140 
    

    首先,从实参和形参的地址可以看出来,实参和形参是两个切片,传参过程中是复制关系,这个不重要,指针传递时也是这样。
    第二,实参和形参指向同一个数组,这个也不重要,指针传递时形参和实参也是指向同一片内存区域。
    但是上面的代码,先扩容,形参slice的底层数组更换了(相当于形参指针指向了新的内存区域,即给指针重新赋值,但是并没有显式进行这个操作,不深入了解切片可能看不出来),所以s[1] = 111不会影响到实参slice的底层数组,修改也就不会体现在实参slice中。下面的代码,先修改,修改发生在形参slice的底层数组上,也是实参slice的底层数组。所以修改体现在实参slice中。
    Append如果发生扩容,相当于修改了指针指向的内存区域。




    更新的分割线,下面是以前的理解,是错误的


      Go数组是值类型,赋值给其他数组和函数传参操作都会复制整个数组数据,如果数组特别大,传来传去浪费大量内存。所以Go搞出了一个切片类型,切片类型的变量是指向一个数组的指针。对切片的操作,就是操作底层的数组。
      这倒是和java很象,java的数组不是基本类型,是引用类型。
      我们知道切片初始化有3种方式。
    第一种:

    arr := [2]int{100, 200} // 指定了大小的就是数组
    sli := []int{100, 200}  // 没指定大小的就是切片
    

    第二种:

    sli := make(int64, 5, 10) // 第三个参数容量可以省略,等于第二个参数长度。底层数组大小等于切片容量
    

    第三种:

    arr := [5]int{1,2,3,4,5}
    sli := arr[1:2] // 初始化切片sli,是数组arr的引用
    

      切片有length和capacity两个概念,length小于等于capacity。capacity不够用了就扩容,扩容的本质是更换一个新的底层数组,反之,不扩容只增加length不会更换底层数组。
      在第三种方式中,sli := arr[1:2]其实隐含了sli := arr[1:2:5]的意思,这里1、2、5都是数组下标,slice[ i : j : k]的length就是j - i,capacity就是k - i,sli的底层数组就是arr,这时候对sli的修改就是对arr的修改。
      这里也不绝对,通过append增加sli的length,直到达到capacity触发扩容,sli就会更换一个新的底层数组(arr不够用了),此后对于sli的修改就不会改变arr了。
    所以,一般建议使用

    sli := arr[1:2:2]
    

      只要append就会切换到新的底层数组,不影响原来的数组。当然你如果没有append直接改变现有的值,还是会改变数组的值。
      时刻记住切片是指向数组的指针。

    相关文章

      网友评论

          本文标题:go切片与数组的关系—修改切片导致数组被修改的问题

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