~$ 存档

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

从切片的地址说起

晚上静下来再写写

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

结论:切片名表示底层数据首地址

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

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

切片名slice实际表示的是底层数据首地址,&slice表示其本身的地址

/* -----------------------------------------------------------------------------------------------------------------*/

2022-5-20更新一下:
切片其实是一个结构体,构造如下:

由于数据靠前,因此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\n",pointer)
        fmt.Printf("%d\n", *(*int)(pointer))    //1,2,3
    }
}
func main() {
    slice := []int{1, 2, 3}
    fmt.Printf("slice:%p,&slice:%p\n", slice, &slice) //slice:0xc000012120,&slice:0xc000004078
    header := (*reflect.SliceHeader)(unsafe.Pointer(&slice)) //转化为*SliceHeader
    //mem(header.Data,header.Len)
    //获取底层数据首地址
    fmt.Printf("底层数据首地址:%p\n", unsafe.Pointer(header.Data)) //底层数据首地址:0xc000012120
    print_int_memory(header.Data, header.Len)
}

结果(注:每次测试结果都不同,以本机为准):

slice:0xc000012120,&slice:0xc000004078
底层数据首地址:0xc000012120
1
2
3

切片做为引用的问题

这一部分参考C语言数组,意思相近。

形参传递

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

 

 

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

容量溢出

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

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

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

容量足够

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

/*
    容量充足,slice未分配的情形
*/
func test_slice(slice []int) {
    fmt.Printf("形参:%p\n", slice) //形参:0xc0000a60a0
    slice = append(slice, []int{4, 5}...)
    fmt.Printf("append之后:%p\n", slice) //append之后:0xc0000a60a0
}
func main() {
    slice := make([]int, 0, 10) //分配10个,使用0个
    slice = append(slice, []int{1, 2, 3}...)
    fmt.Printf("原值:%p\n", 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\n", cap(slice), len(slice))//3,3
    SplitLine()
    make_slice := make([]int, 0, 10)
    fmt.Printf("容量:%d,大小:%d\n", cap(make_slice), len(make_slice))//10,0
    SplitLine()
    make_slice_size := make([]int, 10)
    fmt.Printf("容量:%d,大小:%d\n", 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\n", 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

 

posted on 2021-03-11 22:09  LuoTian  阅读(65)  评论(0编辑  收藏  举报