数组和切片开头可能存在的疑问正文1 .数组和切片2 .传递值和传递引用3 .记忆差异4 .扩展总结
前言
学习Go语言时对序列和切片的存储方法和特性存在疑问,经过一定的实验得出了一些结论,现记录如下。
*后续实验代码省略package main和import 'fmt '
*备忘录是个人理解和互联网资源的摘要,谢谢。
*如果错了,欢迎发表评论
序列和切片有什么不同呢? 与C/C不同的特性,以数组作为函数参数时的更改不会影响原始数组,但切片会影响。 数组的起始元素地址和数组地址相同,但片不同。 数组和切片保存方法的区别是什么? 从数组创建的切片可以按照与数组相同的方式变化,也可以按照不同的方式变化。 切片的两种创建方法之间的差异。 正文1 .数组和切片简单地说,数组是在编译阶段就已经可以决定大小的容器,或者通俗地说是给定长度的向量。 例如,var a [10]int创建长度为10的数组,a :=[ . ] int { 0,1,2,3 }也是长度为4的数组。 数组在定义时使用[…]让编译器自己选择大小。
可以将切片视为不包含长度的“数组”(也没有[…] ) (vara ) []int或由make函数创建的“数组”。
这两者最明显的区别在于,一方决定长度,另一方不确定。 由于Go语言对数据类型的检查非常严格,数组和片不能相互用作对方函数的参数,不同长度的数组也不兼容。 如下所示。
funcalpha(x ) int ) /函数参数为片(/functionbody1) funcbeta ) int ) /函数参数为长度为8的数组)///functionbody2) funcgamma ) ) 数组与切片beta(a ) ) /不同。 从数组到切片只要使用定义即可,从切片到数组可以一个一个地从切片搬运(不知道是否是更有效的方法)。
2 .如果传递值和传递引用位于C/C,且数组为函数参数,则函数内部对数组的变更实际上是对原数组的变更,而Go语言则不是。 如下所示,试图将最初的要素设为9999的函数foo失效了,但切片的安装成功了goo :
funcfoo(x )3) int ) /排列版x(0)=9999 ) /排列的开头要素为9999 ) funcgoo ) x ) int ) /切片版x(0)=9999///同样的操作) }func main 2 2],根据情况b3360=a65: )//b为a的片版goo ) b ) /在调用片版中实现fmt.println } go中数组作为自变量被复制,与其他变量一样,函数
切片为什么不是呢? 究其原因,需要知道两者的内部存储方式,第三个问题需要我们卖关子。
3 .存储差异首先开始我们的实验。
func main () a:=[3]int ) 0,1, 2 ) /数组b :=a[:]//片str1 :=`//格式输出字符串,对于精彩的输出,需要地址a: %p//a的地址a[2]: %p//需要a[2]的地址` sttet () 33660%p`fmt.printf(str1,a,a[0],a[1], a[2]//*输出为以下a :0 xc 00000 a1 c8a [0] :0 xc 00000 a1 c8a [1] :0 xc 00000 a1 d0a [2] :0 xc 0000 a1 D8 */fmt.printt :0 xc 00000 a1 c8b [1] :0 xc 00000 a1 d0b [2] :0 xc 0000 a1 D8 (/)根据实验结果进行数组,而片的各元素也与原始数组对应的元素地址相同。
但是请注意,片b的地址不是b[0]的地址。 这就是切片和序列差异的重要表现。
实际上,切片不是直接指向存储在数组中的第一个元素,而是包含值传递三个部分的结构。 Go将切片的元素访问设计为同一数组的索引访问,使中间的一层结构透明。 这解释了为什么b的地址不等于第一个元素的地址。 这是因为b实际上有结构的地址。 看看源代码,就知道这个了。
p> // 路径~Gosrcruntimeslice.gotype slice struct {array unsafe.Pointer//首元素指针len int//长度cap int//容量}再回到我们的第二点上,实际上切片和数组作为参数调用时都会进行复制,但Go进行的是浅层的复制(不会将指针指向的数组也进行复制),因此切片的首元素指针值被复制进了函数内,后续的变化仍然是原切片。
换句话说,即是从原切片指向对应元素变为了新复制的切片指向对应元素,指的人换了但指的位置不变。我们用实验来佐证这一观点:
可以看到的确除了切片本身地址不同,其各个元素地址完全相同,实验成功。
4.扩容到这里,我们的大多数疑惑都已经消除了,笔者还剩一点的想法。
数组定长以后才便于被函数调用,调用栈才能够留给数组正好的空间来复制到函数内。而另一方面,切片是可以动态收缩的容器,前面的实验告诉我们基于数组构造的切片指向的仍然是原数组,那么切片不断边长,后续加入的元素存放到哪里呢?
接到原数组的后面显然不现实,原因有几点:
另外,我们会发现对从数组创建的切片进行更改时,原数组有时发生变化,有时则不变。
实际上,刚(由数组)创建的切片指针指向切片的首元素,大小为切片的大小(像废话hhhhh),容量为切片的头索引到原数组末端的长度。规范来说,若有如下的切片构造:
则相当于(用于展示,slice无法从外面调用)
var a [length]intvar b slice = slice {array: &a[low]//首元素指针len: high - low//长度cap: length - low//容量}由实验可以很好地得到这一结论:
func print(x []int, low int, high int) {str := `array: %plen: %dcap: %d`fmt.Printf("tlength=5, low=%d, high=%d.", low, high)fmt.Printf(str, &x[0], len(x), cap(x))}func main() {a := [5]int{0, 1, 2, 3, 4}b := a[:]//low=0, high=5, length=5c := a[:2]//low=0, high=2, length=5d := a[1:]//low=1, high=5, length=5print(b, 0, 5)/*输出如下length=5, low=0, high=5.array: 0xc00000c3c0len: 5cap: 5*/print(c, 0, 2)/*输出如下length=5, low=0, high=2.array: 0xc00000c3c0len: 2cap: 5*/print(d, 1, 5)/*输出如下length=5, low=1, high=5.array: 0xc00000c3c8len: 4cap: 4*/}可以看到符合前面的结论。
另一方面,在切片创建完成后不断使用append在尾部加入元素会发生什么呢?结论是:当没有越过原数组的界前会在原数组上不断用新值往后覆盖;越界后切片会将数据复制到新的一个更大的区域(即扩容)后插入新值。
来个例子即可一目了然:
输出结果如下:
&b = 0xc00000c3c0a = [0 1 2 3 4 5]b = [0 1 2] &b = 0xc00000c3c0a = [0 1 2 999 4 5]b = [0 1 2 999] &b = 0xc00000c3c0a = [0 1 2 999 999 5]b = [0 1 2 999 999] &b = 0xc00000c3c0a = [0 1 2 999 999 999]b = [0 1 2 999 999 999] &b = 0xc00001a180a = [0 1 2 999 999 999]b = [0 1 2 999 999 999 999]可以看到每加一次999,原数组都会被覆盖一个999,直至越界后切片搬了新家(&b发生变化)。
在切片由于容量不足而扩容时,可大致认为复制到新的大空间且容量加倍。具体的,我们参见源码:
总结以上策略,优先加倍扩容,若切片过大则按照四分之一向上扩容。
总结数组和切片的细节我们终于大致了解了,在此做一个总结:
数组是在编译阶段就确定长度的一个容器,如此可以让编译器更好分配合适的空间存储数据,但无法灵活扩容。切片则是没有提前约定长度的”数组“,它的长度动态可变。切片本质上是一个结构体,由指针、长度和容量构成。由数组创建的切片指针仍然会指向原数组的对应位置,此时切片和数组对应位置同时变化;而当切片容量不足时会进行复制扩容,之后与原数组就没有关系了。数组被函数调用是传值,对参数数组的改变不会影响原数组;切片则是传引用,会改变原切片的值。