zoukankan      html  css  js  c++  java
  • 切片 //长篇更新

    从切片的地址说起

    晚上静下来再写写

    在前面几篇里面提到,go中的数据多半是复合式的,参照对比是C。切片和字符串类似,它有三个数据量,在sliceheader里面有定义。
    但切片又和字符串有些不同,比如变量名代表什么呢?

    切片名代表什么?

    slice:=[]int{1,2,3}
    fmt.Printf("slice:%p,&slice:%p
    ",slice,&slice)

    代码以指针格式打印slice和&slice,说明这两者都可以做为地址。切片头作为一个结构,它本身有一个地址,而其实质的底层数据也有一个地址。
    切片名slice实际表示的是底层数据首地址,&slice表示其本身的地址。


    下面开始验证

    /*
        打印切片底层数据
     */
    func print_int_memory(ptr uintptr, len int) {
        for i := 0; i < len; i++ {
            pointer := unsafe.Pointer(ptr + uintptr(i*8)) //int为8字节,指针以此偏移
            //fmt.Printf("%p
    ",pointer)
            fmt.Printf("%d
    ", *(*int)(pointer))    //1,2,3
        }
    }
    func main() {
        slice := []int{1, 2, 3}
        fmt.Printf("slice:%p,&slice:%p
    ", slice, &slice)
        header := (*reflect.SliceHeader)(unsafe.Pointer(&slice)) //转化为*SliceHeader
        //mem(header.Data,header.Len)
        fmt.Printf("底层数据首地址:%p
    ", unsafe.Pointer(header.Data)) //获取底层数据首地址
        print_int_memory(header.Data, header.Len)
    }

    切片做为引用的问题

    形参传递

    切片名表示底层数据首地址,在参数传递中,传递的含义相同

    分析:由于slice指向相同的内存区域,所以改变值,原值也会改变

    容量溢出

    上面的例子中,slice未改变,如果使用append会导致内存重新分配,此时形参中的slice会改变,而原值不改变。有关append的知识点都比较了解,这里不再写了。代码如下:

    /*
        容量不足,slice重新分配的情形
     */
    func test_slice(slice []int) {
        fmt.Printf("形参:%p
    ", slice) //形参:0xc0000123c0
        slice = append(slice, []int{4, 5}...)
        fmt.Printf("append之后:%p
    ", slice) //append之后:0xc00000c330
    }
    func main() {
        slice := []int{1, 2, 3}
        fmt.Printf("原值:%p
    ", slice) //原值:0xc0000123c0
        test_slice(slice)
        fmt.Println(slice) //[1 2 3]
    }

    分析:形参处于堆栈中,容量不足导致内存重新分配,slice的值发生改变,而原值未变

    容量足够

    如果容量足够,那么slice就不会发生重分配

    /*
        容量充足,slice未分配的情形
    */
    func test_slice(slice []int) {
        fmt.Printf("形参:%p
    ", slice) //形参:0xc0000a60a0
        slice = append(slice, []int{4, 5}...)
        fmt.Printf("append之后:%p
    ", slice) //append之后:0xc0000a60a0
    }
    func main() {
        slice := make([]int, 0, 10) //分配10个,使用0个
        slice = append(slice, []int{1, 2, 3}...)
        fmt.Printf("原值:%p
    ", slice) //原值:0xc0000a60a0
        test_slice(slice)
        fmt.Println(slice) //[1 2 3]
        slice = slice[:5]  //重新设置切片长度,使得数据可见
        fmt.Println(slice) //[1 2 3 4 5]
    }

    分析:这段代码有一个注意点,如果不使用slice=slice[:5]验证,可能误以为原值未改变,其实质是变化的.

    本质

    怎么能解释这些问题呢,下面给一个简单的模型帮助记忆,考虑两个结构体赋值的问题:
    结构体样式

    type T struct{
        *S
    }

    类型T内嵌一个成员指针S,如果有两个T类型的实例赋值,比如t1=t2,可知t1和t2绝对是不同的,但是指针也像普通值一样被复制过去了,就导致常说的浅复制

    现在将思路转回到切片,切片的头结构包含一个底层数组的指针。因此,在复制时和结构体一样,两个切片本身绝对是不同的,可以通过%p打印&T查看。但是由于两个切片拥有相同的底层指针,
    因此导致了上面的各种问题。

    比如
    S1【长度,容量,指针】
    S2【长度,容量,指针】
    S1=S2,假如S2的指针指向产生变化,那么对S1无影响。又比如,S1进行append追加数据(充足的情形下),会可能导致S1的长度发生变化,但是S1的变化并未导致S2的变化,因为它们各自拥有一份,所以
    在上述例子中,需要把切片的长度进行修正


    一个重要的地方在于,在使用append时,S1和S2被隐性的转化为底层数据首地址......

    切片初始容量

    /*
        测试slice容量和大小
     */
    func main() {
        slice := []int{1, 2, 3}
        fmt.Printf("容量:%d,大小:%d
    ", cap(slice), len(slice))//3,3
        SplitLine()
        make_slice := make([]int, 0, 10)
        fmt.Printf("容量:%d,大小:%d
    ", cap(make_slice), len(make_slice))//10,0
        SplitLine()
        make_slice_size := make([]int, 10)
        fmt.Printf("容量:%d,大小:%d
    ", cap(make_slice_size), len(make_slice_size))//10,10
    }

    参考结果,区别三种不同的初始化方式得到的不同结果


    其中,make([]int,10)这种形式比较奇怪,结果为:长度为10,容量为10。既然长度已经分配了,有什么用呢?


    可以将这种形式认为是一个初始化了数据的“数组"...,在后面的切片copy中再举例解释。

    切片的分割

    首先看下测试代码

    /*
        slice的分割测试
    */
    func main() {
        slice := []int{1, 2, 3, 4, 5, 6}
        split_slice := slice[:3]
        fmt.Println("split_slice:", split_slice)                                      //[1,2,3]
        fmt.Printf("split_slice's 容量:%d,大小:%d
    ", cap(split_slice), len(split_slice)) //6,3
        split_slice[0] = 10
        fmt.Println("slice:", slice) //[10 2 3 4 5 6]
    
        split_slice = append(split_slice, []int{7, 8, 9}...)
        fmt.Println("split_slice:", split_slice) //[10 2 3 7 8 9]
        fmt.Println("slice:", slice)
    }

    这段代码主要有两点需要注意


    1、由上面知道,切片名表示底层数据首地址,这个理解非常重要,是所有测试的核心,理解了这一点也就理解了代码为什么会这样
    2、从切片中分割得到的切片容量和原切片相同。怎么理解呢?假如B是从A中分割出来的,我们可以认为B在底层的分配上,至少会和A一样大。考虑一个极端情形,B完全等于A
         或者理解为,仅仅是将底层数据地址赋值,其它原样拷贝
         这样理解记忆会更深些

    [分析]:有了上面两条的理解,代码就比较容易理解。split_slice初始容量等于slice,因此再次append时并未分配。又由于两者都是指向底层数据首地址,因此split_slice改变,那么slice也会跟着改变
    假如split_slice追加数据之后最终大于6,那么会导致重分配,split_slice又重新指向新地址,而原slice不会变化000

    切片copy

    默认拷贝和指定位置拷贝

    实例:带缓冲的Write

    特例:从字符串拷贝到[]byte

  • 相关阅读:
    ChsDet is a Charset Detector (检测字符编码)
    Microsoft Fakes进行单元测试
    区域及分离、Js压缩、css、jquery扩展
    服务端软件的服务品质
    警告: [SetPropertiesRule]{Server/Service/Engine/Host/Context}
    开发之技能
    分布式
    工具集
    Visual Studio Code
    Autofac 入门
  • 原文地址:https://www.cnblogs.com/tinaluo/p/14520922.html
Copyright © 2011-2022 走看看