【Go语言】一文了解数组和切片区别(内存结构) - Go语言中文社区

【Go语言】一文了解数组和切片区别(内存结构)


引子

数组和切片是我们较为常用的数据结构,正是因为他们的普遍,其中有许多小问题都被我们忽略掉了,比如说:
1.在作为参数传递时,为什么切片的改动会使得原来的实参也发生变化,而数组的改动就不会影响到实参
2.为什么数组的类型会因为长度不同而无法兼容,但是切片却可以随意传递,两者的内存布局到底是什么样的
3.为什么数组偏向静态的不能追加,而切片却可以动态扩容

定义

先从定义开始
数组常用的定义方法:

var arr_1 = [...]int{1,2,3}//[...]int只能在定义时用,不能在返回时参数,和形参使用

var arr_2 [1]int//在定义时,这个容量必须是恒定数字,不能是一个int变量,不然报错:x:=1,  arr:=[x]int就不能

通用结构就是:
var 名称 [数组长度]元素类型
var arr [3]int

切片的定义方法:

var slice_1 = make([]int,10,10)//通过make创建
var slice_2 []int = arr_1[0:3]//截取数组的相关片段

通用结构:
var 名称 [ ]元素类型
var slice [ ]int

内存结构(!!!)

其实最快了解数组和切片的方式就是掌握他们的内存布局
我们先打印一下上面的数组和切片的相关数据:

fmt.Printf("arr1首地址:%pn,arr1【0】首地址:%pn,slice首地址:%pn,slice【0】地址%pn",&arr_1,&arr_1[0],&slice_1,&slice_1[0])
	/*arr1首地址:0xc0420480a0
     arr1【0】首地址:0xc0420480a0
	 slice首地址:0xc0420443e0
	 slice【0】地址0xc0420820a0
	 

那么问题来了,为什么数组的首地址和他头号元素的地址相同,而切片的首地址和他头号元素不同呢???
其实是这个样子的:

首先我们来看看数组的内存结构:
在这里插入图片描述
数组其实就一段大小固定的空间,他的大小就是在初始化时给的长度len,根据长度len和元素类型大小(例如元素为int64也就是8字节)就可以很轻易的算出空间的完整大小,在完全确定后就会直接敲定无法改变。他的头号元素也是整个空间的头,地址一致。

正因如此,数组具有以下特点:
1.长度固定,无法追加元素,因为空间大小就给了这么多,所以[5]int和[10]int两个数组表示的是不同的类型自然无法兼容
2.数组是值类型的,在传递时采用值拷贝:

func test(arr [3]int){//数组类型参数不能写成[...]int,这个只能用于定义时,且不能写[]int,这是切片类型
	fmt.Printf("%p",&arr)//参数地址:0xc042048160,,arr1首地址:0xc0420480a0,两者不同且修改后不影响
	for index,value:=range arr{
		arr[index] -= value
	}
	fmt.Println("清零数组:",arr)//数组由于是定死的,所以也是值拷贝,只会在这个方法对应的栈帧里的数组进行一个操作,不会影响外边
}

var arr_1 = [...]int{1,2,3}
test(arr_1)//数组值拷贝,我在里面清零后,原数组不变
fmt.Println("源数组:",arr_1)

/*清零数组: [0 0 0]
源数组: [1 2 3 ]*/

看结果就能证明这一点,其实本质还是在main中调用test方法,该方法栈帧入了栈后将实参arr1传过去,打印发现地址不同但是修改完成了后,实参并未受到影响。
(这里的意思就是arr1是实参,调用函数test传递的arr是函数对应的形参,他相当于从开了一片空间,把arr1的空间中的值给复制一份到arr空间然后传递,所以从内存分布上来讲,他俩肯定空间不同,那地址也必然不同,当参数传过来后,通过修改形参并没影响到实参,那就说明必然是值传递),所以要想修改实参中的数组数据,就只能传地址过去。

切片的内存布局
在这里插入图片描述
切片本质就是一个结构体,他里面包含三部分:address + len + cap,
address: 就是他指向的内部数组或数组某个地方
len:是当前的元素个数
cap:可容纳元素总容量大小
也不难理解为什么cap>=len
正是如此,切片本质上是一个引用空间,该空间和元素空间完全是两个空间,所以切片的首地址和头号元素的首地址完全不同。

func test_3(a []int){
 	fmt.Printf("%p",&a)//0xc042002440
	a[0] = 999
}
 var slice_1 = make([]int,10,10)
 test_3(slice_1)
 fmt.Printf("%p",&slice_1)//0xc042002420
 fmt.Println(slice_1)//[999 0 0 0 0 0 0 0 0 0]

我们可以看到,实参slice_1在函数调用时作为参数传过去了,而a的地址和slice_1地址不同,也就是两个不同的空间,但是对a进行操作后,最终打印slice_1我们发现他的值也变化了,故slice_1空间的内容复制给a空间的是地址值,然后a对该地址指向的空间进行操作也会影响到slice_1,所以是引用传递。

值得一提的是:通过make创建的切片和通过数组[:]分的切片是有区别的!

1.直接var slice_2 []int = arr_1[1:3]切分数组产生的切片其实就是直接对这个数组的引用,这时当切片中的值修改时会影响到原来的数组值:

	 var arr_1 = [...]int{1,2,3}
	 var slice_2 []int = arr_1[1:3]
	 
	 fmt.Println(arr_1)//[1 2 3]
	 fmt.Println(slice_2)//[2 3]
	 test_3(slice_2)
	 fmt.Println(slice_2)//[999 3]
	 fmt.Println(arr_1)//[1 999 3]

2.而通过make创建其实本质上也是先偷偷的创建一个内部数组,然后该切片在对该数组进行操作。

 	  var slice_1 = make([]int,10,10)
 	  xx := slice_1
	 
	  fmt.Println(slice_1)//[0 0 0 0 0 0 0 0 0 0]
	  xx[0] = 260
	  fmt.Println(slice_1)//[260 0 0 0 0 0 0 0 0 0]
	  fmt.Printf("xx空间地址:%pn slice1地址%pn xx[0]%pn,slice[0]%pn",&xx,&slice_1,&xx[0],&slice_1[0])
/*	xx空间地址:0xc042044420
 	slice1地址0xc0420443e0
 	xx[0]0xc0420820a0
	slice[0]0xc0420820a0
	不难看出,把slice1赋值给新生成的xx,他俩虽然不是同一空间,但是里面内容一致,且指向同一个内部数组,当xx修改0时,slice的也变了
	xx := slice1 赋值过去本质就是把xx空间内容赋值给xx,就是把它里面的add+len+cap给赋值过去xx,所以俩add一致维护同一个数组
*/

内置方法

1.len(数组/切片):返回元素个数,也就是长度

fmt.Println(len(arr_1),len(slice_1))//3 10

2.cap(数组/切片):返回总容量大小

fmt.Println(cap(arr_1),cap(slice_1))//3 10

3.append():给切片追加元素

	var slice_1 = make([]int,10,10)
	var slice_2 []int = arr_1[1:3]
	var slice_3 []int= append(slice_2,slice_1...)//俩切片互相追加,则必须加...
	fmt.Println(slice_3)
	fmt.Println(len(slice_1),cap(slice_2),len(slice_3),cap(slice_3))
	/* [2 3 0 0 0 0 0 0 0 0 0 0]
	   10 2 12 12*/

对切片追加会判断是否已经超了cap,是的话就会扩容

slice_2 = append(slice_2,slice_1...)
fmt.Println(slice_2)//[3 4 0 0 0 0 0 0 0 0 0 0]

扩容本质就是底层从新创建一个满足当前大小cap的数组(新空间),并且把原数组中的值给拷贝过来,在让切片指针指向新数组。
这个也很好证明,因为刚才已经证明过了:操作slice_2【0】的时候对slice_2修改会影响到arr1,现在给slice_2扩容了后再操作他,看看会不会对arr1产生影响:如果扩容后修改slice_2【0】仍然会使得arr1【1】变化,那就说明仍然是原数组空间扩容。如果不会是的arr【1】变化,则证明扩容后的数组是新数组而不是原数组

	 var arr_1 = [...]int{1,2,3}
	 var slice_1 = make([]int,10,10)
	 var slice_2 []int = arr_1[1:3]
	 
	 slice_2 = append(slice_2,slice_1...)
	 
	 fmt.Println(arr_1)//[1 2 3 ]
	 fmt.Println(slice_2)//[2 3 0 0 0 0 0 0 0 0 0 0]
	 test_3(slice_2)
	
	 fmt.Println(slice_2)//[999 3 0 0 0 0 0 0 0 0 0 0]
	 fmt.Println(arr_1)//[1 2 3]

我们可以发现这次arr1的值并没有发生改变,也就是产生了新数组,然后slice_2的引用指向了新数组。
在这里插入图片描述

总结

1.数组是静态定死的len不可变,而切片是动态的可以继续扩容追加
2.数组的类型是[len]int,且不同len长度的数组相互不兼容,而切片是稳定的[]int类型
3.数组是值传递,空间内容就是数组的每个元素,他的len也就是cap; 切片是引用传递,其空间内容:address + len + cap,容量可以扩容且>=len

如有问题欢迎指出,共勉互进!

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_44938441/article/details/110424749
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢