从切片的地址说起
晚上静下来再写写
在前面几篇里面提到,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