Slice的学习:
切片是 Go 中的一种基本的数据结构,可以用来管理数据集合。
设计想法是由 动态数组 概念而来。为了开发者可以更加方便的使一个数据结构可以自动增加或者减少。但是切片本身并不是 动态数据 或者 数组指针。
切片常见的操作有 reslice、append、copy,切片还具有可索引、可迭代的优秀特性。
“冷”知识:
1. slice指向数组,对数组进行切片处理后,得到的结果地址和与之相对应的数组元素地址是相同的,共享数组的存储空间。
2. Go中的数组是值类型,赋值和函数传参都是值复制。
也正是因为这个特点,每次传参都需要数组被复制一遍,这样的话会耗费掉大量的内存,所以,函数传参用数组的指针最为合适。但是若传入了指针,对原数组的指针指向改变了,函数里面的指针都会改变。
切片的优势便体现出来,用切片传输数组参数,既可以节约内存,也可以合理处理好共享内存的问题。
切片是引用传递,所以它们不需要使用额外的内存并且比使用数组更有效率。
关于切片:
切片内部实现的数据结构是 通过指针引用底层数组,设定了相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,可以将它的工作机制看作类似于数组指针的一种封装。
切片实际上是对目标数组一个连续片段的引用,所以切片是一个引用类型(类似python里面的list类型)。这个片段可以是 整个数组 或是 由起始和终止索引标识截取的子集(左闭右开,右边索引对应的值不取),切片提供了一个指向数组的动态窗口。
小测试:
从go的内存地址中构造一个Slice:
make创建slice:
func makeslice(et *_type, len, cap int) slice {
// 根据切片的数据类型,获取切片的最大容量
maxElements := maxSliceCap(et.size)
// 比较切片的长度,长度值域应该在[0,maxElements]之间
if len < 0 || uintptr(len) > maxElements {
panic(errorString("makeslice: len out of range"))
}
// 比较切片的容量,容量值域应该在[len,maxElements]之间
if cap < len || uintptr(cap) > maxElements {
panic(errorString("makeslice: cap out of range"))
}
// 根据切片的容量申请内存
p := mallocgc(et.size*uintptr(cap), et, true)
// 返回申请好内存的切片的首地址
return slice{p, len, cap}
}
两种创建slice切片:
// 第一种 利用 make 创建 未定义初值
slice := make([]int, 4, 6)
// 第二种 利用 字面量 创建 可以赋予初值
slice := []int{10, 20, 30, 40}
// 注意:[] 中不要写内容,不然就变成了数组
切片扩容:
当一个切片的容量满了,就需要进行扩容了,怎么扩,策略是什么?
(扩容时的策略,扩容是生成全新的内存地址还是在原来的地址后追加)
扩容策略:
1. 首先判断,如果新申请的容量cap大于两倍的旧容量,那么最终容量就是新申请的容量
2. 如果新申请的容量cap并不大于两倍的旧容量,且旧切片的长度小于1024,那么最终容量是旧容量的两倍
3. 如果旧切片的长度大于等于1024,那么最终容量需要从旧容量开始循环增加原来的 四分之一 ,直到最终容量大于等于新申请容量
4. 最后如果最终容量计算值溢出,那么最终容量就是新申请容量
扩容安全:
其实分了两种情况
1. 当原数组还有容量可以直接进行扩容时,执行append操作后,会在原数组上直接操作,扩容以后的数组还是指向原来的数组,并没有新建一个新的数组。
因为扩容前后的数组都是同一个,所以这也就导致了新的切片修改了一个值,就影响到了老的切片,甚至影响到了原数组。
**************在用字面量创建切片的时候,cap的值一定要保持清醒**************
2. 当原数组的长度 等于 容量,也就是容量到达了最大值时,再想扩容,Go默认会先开一片内存区域,把原有的值拷贝过来,再执行append()操作。