go语言 --- 切片与数组

导言

在下面,我会先说说切片和数组的关系,之后再提及切片的追加、切片的拷贝和切片作为参数的一些注意点,至于切片元素的删除等基本操作,本文不涉及,想要了解的话可以看go语言切片


切片与数组

切片与数组的结构

  • 切片的结构

    切片在内存中的表现其实是一个结构体,该结构体中有三个成员变量,共占用了24字节的内存空间。(下面会说为什么是24字节)

    我们先可以打开 go语言源码包src中的 runtime/slice.go 查看切片结构。

    type slice struct {
    	array unsafe.Pointer	// 指向地址
    	len   int				// 长度
    	cap   int				// 容量
    }
    

    由以上代码可以看出,切片有3个属性(即成员变量):

    • 指向地址
    • 长度
    • 容量

    对于这3个属性我先不作解释,说了数组后再解释。

    现在来回答下为什么切片占用24字节的内存空间。因为我的电脑是64位的,unsafe.Pointer类型变量占用8个字节,int类型变量占用8个字节,所以算起来切片所占内存空间大小为8 + 8 * 2 = 24 字节

    当然,除了这三个属性,切片还有一个地址,用以表示切片在内存中的位置,我简称它为切片内存地址。
    注意:切片指向地址和切片内存地址是不一样的!

  • 数组的结构
    数组在内存中的表现为一块连续的内存空间,该内存空间的大小等于数组元素个数 * 每个元素所占字节数,比如一个长度为10int32的数组,其所占用的内存空间大小为10 * 4 = 40 字节。(int32类型的变量占用4个字节的内存空间)

  • 二者的对比
    在这里插入图片描述

切片与数组的关系

切片是数组的引用,如果该引用部分发生改变,那么切片和数组都会发生改变。什么意思呢?下面通过实例说明。

我们现在创建一个数组array,包含五个元素。然后创建一个切片slice指向它。

array := [5]int{1,2,3,4,5}	// 创建数组
slice := []int{}			// 创建切片(此时切片指向的地址为nil)
slice = array[0:]			// 执行这语句后,切片指向的地址为数组首元素的地址,而且
							// 切片的长度会等于数组的长度

可以看出,数组的创建和切片的创建差不多,但是数组创建必须给定创建的空间大小,如上面要创建5个元素的数组,那必须写为[5]int{元素...}。而切片的创建可以不用给定创建的空间大小,如上面要创建一个切片,只需要写为[]int{}

接下来我们来输出二者的信息。

fmt.Println("数组所在内存地址: ",unsafe.Pointer(&array))
fmt.Println("切片所在内存地址: ",unsafe.Pointer(&slice))
fmt.Printf("切片指向的内存地址: %p\n",slice)
fmt.Printf("切片的长度: %d\n", len(slice))
fmt.Printf("切片的容量: %d\n",cap(slice))

// 输出内容
// 数组所在内存地址:  0xc04200a330
// 切片所在内存地址:  0xc042002440
// 切片指向的内存地址: 0xc04200a330
// 切片的长度: 5
// 切片的容量: 5

可以看出, 切片指向的内存地址就是数组所在的内存地址。在此时,slice就是整个array的引用,slice中元素的修改将会影响array中对应的元素(等等在下部分验证)。

二者在内存中的关系如下:

以上是切片指向array[0] 的情况,假如切片指向array[1]呢?

array := [5]int{1,2,3,4,5}
slice := array[1:]			// 与上面的代码相比,只有这做了更改

fmt.Println("数组所在内存地址: ",unsafe.Pointer(&array))
fmt.Println("切片所在内存地址: ",unsafe.Pointer(&slice))
fmt.Printf("切片指向的内存地址: %p\n",slice)
fmt.Printf("切片的长度: %d\n", len(slice))
fmt.Printf("切片的容量: %d\n",cap(slice))

// 输出:
// 数组所在内存地址:  0xc04200a330
// 切片所在内存地址:  0xc042002440
// 切片指向的内存地址: 0xc04200a338
// 切片的长度: 4
// 切片的容量: 4

可以看出,slice指向的地址变为了array[1]的地址。在此时,slice就是array[1]array[4]的引用。
接下来看看两段代码在内存中的对比。
在这里插入图片描述
注意:在上述代码中,slice = array[0:]0是可以省略的,即可以写为slice = array[:]。而且,如果slice要引用array[1]array[3],我们为切片赋值时,可以写为slice = array[1:4],即数组的[1,4)元素。

如果理解了上面,那么下面的一些现象就可以解答了。我们来验证一下,切片元素的修改是否会影响数组中对应的元素。

切片元素的修改对数组的影响

接下来我们创建一个数组array并让一个切片slice引用该数组。我们看看修改某个切片元素值的前后,数组和切片的元素值的变化情况。

func main() {
	array := [5]int{1,2,3,4,5}
	slice := array[:]

	fmt.Println("修改切片之前:")
	fmt.Println("	切片:",slice)
	fmt.Println("	数组:",array)

	slice[0] = 8	// 把切片元素0的值改为8
	fmt.Println("修改切片之后:")
	fmt.Println("	切片:",slice)
	fmt.Println("	数组:",array)
	
	// 输出
	// 修改切片之前:
	//	切片: [1 2 3 4 5]
	//	数组: [1 2 3 4 5]
	// 修改切片之后:
	//	切片: [8 2 3 4 5]
	//	数组: [8 2 3 4 5]
}

可以看出,我们作用在切片上的修改影响到了数组。这是因为当我们修改切片元素的时候,实际上作用的是数组的内存空间,即当我们修改slice[0]时,实际上修改的是array[0]同理,假如我们修改array[0],那slice[0]也会发生改变,下面验证一下。

func main() {
	array := [5]int{1,2,3,4,5}
	slice := array[:]

	fmt.Println("修改数组之前:")
	fmt.Println("	切片:",slice)
	fmt.Println("	数组:",array)

	array[0] = -8	// 把数组元素0的值改为8
	fmt.Println("修改数组之后:")
	fmt.Println("	切片:",slice)
	fmt.Println("	数组:",array)

	// 输出
	// 修改数组之前:
	//	切片: [1 2 3 4 5]
	//	数组: [1 2 3 4 5]
	// 修改数组之后:
	//	切片: [-8 2 3 4 5]
	//	数组: [-8 2 3 4 5]
}

注意:在上述代码中,切片引用了整个数组,所以slice[0]的修改本质上是array[0]的修改,如果slice引用的是array[1]array[3],那么slice[0]的修改会转变为对array[1]的修改,而反过来,如果我们修改array[1]slice[0]的值也会改变,而对array[0]array[4]的修改将不会影响slice

小结

  • 切片指向某个数组,是数组的引用,其构建在数组的基础上
  • 切片的修改作用于数组的内存区域

切片的追加 — append函数

使用append函数,我们可以实现向切片末尾追加元素的功能,但有以下注意点。

  • append函数的返回值是一个内存地址,该地址是内存中一片连续空闲空间的首地址
  • append(slice, 所添加元素),假如此时slice的长度 < slice的容量,那么append函数返回的内存地址等于原slice指向的内存地址。而假如此时slice的长度 == slice的容量,那么append函数返回的内存地址不等于原slice指向的内存地址。
  • 实现切片元素追加:slice = append(slice, 所添加元素)

接下来我们验证一下上面的注意点。
我们创建一个切片slice,该切片长度为2且容量为3,再为切片追加2个元素。

func main() {
	slice := make([]int,2,3)	// 另外一种创建切片的方式,表示创建一个长度为2,容量为3的切片
	fmt.Println("切片的内容: ",slice)
	fmt.Println("			内存地址: ",unsafe.Pointer(&slice))
	fmt.Printf("			指向的内存地址: %p\n",slice)
	fmt.Printf("			切片的长度: %d\n", len(slice))
	fmt.Printf("			切片的容量: %d\n",cap(slice))

	slice = append(slice,5)
	fmt.Println("追加一个元素5后")
	fmt.Println("切片的内容: ",slice)
	fmt.Println("			内存地址: ",unsafe.Pointer(&slice))
	fmt.Printf("			指向的内存地址: %p\n",slice)
	fmt.Printf("			切片的长度: %d\n", len(slice))
	fmt.Printf("			切片的容量: %d\n",cap(slice))


	slice = append(slice,10)
	fmt.Println("再追加一个元素10后")
	fmt.Println("切片的内容: ",slice)
	fmt.Println("			内存地址: ",unsafe.Pointer(&slice))
	fmt.Printf("			指向的内存地址: %p\n",slice)
	fmt.Printf("			切片的长度: %d\n", len(slice))
	fmt.Printf("			切片的容量: %d\n",cap(slice))


	// 输出
	// 切片的内容:  [0 0]
	//			内存地址:  0xc04204e3e0
	//			指向的内存地址: 0xc0420520a0
	//			切片的长度: 2
	//			切片的容量: 3
	// 追加一个元素5后
	// 切片的内容:  [0 0 5]
	//			内存地址:  0xc04204e3e0
	//			指向的内存地址: 0xc0420520a0
	//			切片的长度: 3
	//			切片的容量: 3
	// 再追加一个元素10后
	// 切片的内容:  [0 0 5 10]
	//			内存地址:  0xc04204e3e0
	//			指向的内存地址: 0xc042068060
	//			切片的长度: 4
	//			切片的容量: 6
}

大家可以看看上面的输出信息,看看追加元素前后slice的变化。接下来,我提出一个问题。

想一想,为什么切片长度等于容量时,使用append函数返回的内存地址与原切片指向地址不一样呢?

你们应该想出来了,其实就是原切片所指向的内存空间装不下追加元素后的切片的所有元素了,所以append函数需要再找出一块足够大的内存空间并返回该内存空间的首地址,这样追加元素后的切片才能把所有的元素放置在该内存空间中。


切片的拷贝 — =赋值 与 copy函数

使用copy函数 ,我们可以把源切片的元素拷贝给目标切片,但在说copy函数前,我们先来看看使用=为切片赋值的情况。

下面我们创建一个长度为3的切片slice1,使用=赋值给slice2

func main() {
	slice1 := []int{1,2,3}
	fmt.Println("slice1的内容: ",slice1)
	fmt.Println("			内存地址: ",unsafe.Pointer(&slice1))
	fmt.Printf("			指向的内存地址: %p\n",slice1)
	fmt.Printf("			切片的长度: %d\n", len(slice1))
	fmt.Printf("			切片的容量: %d\n",cap(slice1))
	
	slice2:= slice1
	fmt.Println("slice2的内容: ",slice2)
	fmt.Println("			内存地址: ",unsafe.Pointer(&slice2))
	fmt.Printf("			指向的内存地址: %p\n",slice2)
	fmt.Printf("			切片的长度: %d\n", len(slice2))
	fmt.Printf("			切片的容量: %d\n",cap(slice2))
	
	// 输出
	// slice1的内容:  [1 2 3]
	//			内存地址:  0xc042002440
	//			指向的内存地址: 0xc04200c360
	//			切片的长度: 3
	//			切片的容量: 3
	// slice2的内容:  [1 2 3]
	//			内存地址:  0xc0420024c0
	//			指向的内存地址: 0xc04200c360
	//			切片的长度: 3
	//			切片的容量: 3
}

可以看出,使用=赋值,两个切片所指向的内存地址是一样的。

这会导致什么问题呢?

这会使,当我们修改slice1的元素的值,slice2中对应的元素的值也会发生改变。同理,当我们修改slice2的元素的值,slice1对应的元素的值也会发生改变 (后面会给出验证)。我们称这种拷贝为浅拷贝

很多时候,我们不希望出现这种情况,我们只是希望它们之间的元素是一样的,那此时我们可以使用copy函数,下面是使用copy函数的注意点。

  • copy函数接收2个参数,第一个参数是目标切片,第二个参数是源切片,其功能是把源切片的元素拷贝到目标切片中 (方向:<-),用法:copy(目标切片,源切片)
  • copy函数只能拷贝,两个切片中最小长度的元素个数,比如目标切片有3个元素,而源切片有5个,那copy函数只会把源切片的前3个元素拷贝给目标切片。而假如目标切片有5个元素,而源切片有3个,那copy函数也只会把源切片的前3个元素拷贝给目标切片。

下面我们创建一个长度为3的切片slice3,再使用copy函数把其元素拷贝给slice4

func main() {
	slice3 := []int{1,2,3}
	fmt.Println("slice3的内容: ",slice3)
	fmt.Println("			内存地址: ",unsafe.Pointer(&slice3))
	fmt.Printf("			指向的内存地址: %p\n",slice3)
	fmt.Printf("			切片的长度: %d\n", len(slice3))
	fmt.Printf("			切片的容量: %d\n",cap(slice3))

	slice4:= make([]int, len(slice3))	// 其实执行了这一步后,slice4和slice3
										// 所指向的内存地址就不一样了
	copy(slice4,slice3)

	fmt.Println("slice4的内容: ",slice4)
	fmt.Println("			内存地址: ",unsafe.Pointer(&slice4))
	fmt.Printf("			指向的内存地址: %p\n",slice4)
	fmt.Printf("			切片的长度: %d\n", len(slice4))
	fmt.Printf("			切片的容量: %d\n",cap(slice4))

	// 输出
	// slice3的内容:  [1 2 3]
	//			内存地址:  0xc042002440
	//			指向的内存地址: 0xc04200c360
	//			切片的长度: 3
	//			切片的容量: 3
	// slice4的内容:  [1 2 3]
	//			内存地址:  0xc0420024c0
	//			指向的内存地址: 0xc04200c3a0
	//			切片的长度: 3
	//			切片的容量: 3

}

可以看出,使用copy函数,两个切片所指向的内存地址是不一样的。所以slice3slice4是独立的,二者不会相互影响。我们称这种拷贝为深拷贝。 (其实使用copy函数类似于使用一个循环,把slice3的元素一个一个赋值给slice4)

下面我们来验证一下,=赋值和使用copy函数拷贝分别是否会使切片之间互相影响。

  • =赋值
    func main() {
    	slice1 := []int{1,2,3}
    	slice2:= slice1
    	slice1[0] = 5
    	fmt.Println("通过等号赋值把slice1[0]改为5后")
    	fmt.Println("	slice1的内容: ",slice1)
    	fmt.Println("	slice2的内容: ",slice2)
    
    	slice2[1] = 10
    	fmt.Println("通过等号赋值把slice2[1]改为10后")
    	fmt.Println("	slice1的内容: ",slice1)
    	fmt.Println("	slice2的内容: ",slice2)
    
    	// 通过等号赋值把slice1[0]改为5后
    	//	slice1的内容:  [5 2 3]
    	//	slice2的内容:  [5 2 3]
    	// 通过等号赋值把slice2[1]改为10后
    	//	slice1的内容:  [5 10 3]
    	//	slice2的内容:  [5 10 3]
    }
    
    可以看出,使用=赋值的切片,源切片和目标切片相互影响。本质上是因为它们指向了相同的内存空间。
  • copy函数
    func main() {
    	slice3 := []int{1,2,3}
    	slice4:= make([]int,3)
    	copy(slice4, slice3)
    	slice3[0] = 5
    	fmt.Println("通过等号赋值把slice3[0]改为5后")
    	fmt.Println("	slice3的内容: ",slice3)
    	fmt.Println("	slice4的内容: ",slice4)
    
    	slice4[1] = 10
    	fmt.Println("通过等号赋值把slice4[1]改为10后")
    	fmt.Println("	slice3的内容: ",slice3)
    	fmt.Println("	slice4的内容: ",slice4)
    
    	// 通过等号赋值把slice3[0]改为5后
    	//	slice3的内容:  [5 2 3]
    	//	slice4的内容:  [1 2 3]
    	// 通过等号赋值把slice4[1]改为10后
    	//	slice3的内容:  [5 2 3]
    	//	slice4的内容:  [1 10 3]
    }
    
    可以看出,使用copy函数拷贝的切片,源切片和目标切片不会相互影响。本质上是因为它们指向了不同的内存空间。

切片作为参数

接下来我们创建一个切片slice并赋值,再将它作为实参传入show函数,来看看函数内外的切片的信息有何不同。

func show(slice []int){
	fmt.Println("函数内slice的内容: ",slice)
	fmt.Println("			内存地址: ",unsafe.Pointer(&slice))
	fmt.Printf("			指向的内存地址: %p\n",slice)
	fmt.Printf("			切片的长度: %d\n", len(slice))
	fmt.Printf("			切片的容量: %d\n",cap(slice))
}


func main() {
	slice := []int{1,2,3}
	fmt.Println("函数外slice的内容: ",slice)
	fmt.Println("			内存地址: ",unsafe.Pointer(&slice))
	fmt.Printf("			指向的内存地址: %p\n",slice)
	fmt.Printf("			切片的长度: %d\n", len(slice))
	fmt.Printf("			切片的容量: %d\n",cap(slice))

	show(slice)
	
	// 输出
	// 函数外slice的内容:  [1 2 3]
	//			内存地址:  0xc04204e3e0
	//			指向的内存地址: 0xc0420520a0
	//			切片的长度: 3
	//			切片的容量: 3
	// 函数内slice的内容:  [1 2 3]
	//			内存地址:  0xc04204e460
	//			指向的内存地址: 0xc0420520a0
	//			切片的长度: 3
	//			切片的容量: 3
	// 这种情况类似于使用等号赋值,函数形参和实参是两个不同的变量,所以有不同的内存地址,
	// 但是他们的值(对于切片来说就是指向地址,长度以及容量)是一样的
}

可以看出,函数外的slice和函数内的slice除内存地址不同外,其它的信息都相同。
这是因为,切片作为参数传入函数类似于=赋值,赋值后,它们的值是一样的。(对于切片来说,切片的值就是切片指向的地址、长度以及容量)

下面考虑一个问题,当我们在函数内修改切片元素的值后,函数外的切片是否会受到影响?

答案是肯定的,因为本质上它们指向的内存地址是一样的,它们使用同一块内存空间。验证如下。

func change(slice []int){
	slice[0] = 5
	fmt.Println("赋值后,函数内slice的内容: ",slice)
}


func main() {
	slice := []int{1,2,3}
	fmt.Println("函数外slice的内容: ",slice)
	change(slice)
	fmt.Println("函数执行后,函数外slice的内容: ",slice)
	
	// 输出
	// 函数外slice的内容:  [1 2 3]
	// 赋值后,函数内slice的内容:  [5 2 3]
	// 函数执行后,函数外slice的内容:  [5 2 3]
}

综合情况

接下来我们来做个题,看看自己是否有些收获。

func function(slice []int){
	slice = append(slice, 4)
	slice[0] = 5
}
func main() {
	slice1 := []int{1,2,3}
	function(slice1)

	slice2 := make([]int,4)
	copy(slice2,slice1)

	fmt.Print(slice2)
	// 请问输出是什么?
	// A. [1 2 3 4]
	// B. [5 2 3 4]
	// C. [5 2 3 0]
	// D. [1 2 3 0]
}

如果你看完了这一篇,做了这个题,欢迎把答案在评论区打出喔~

总结

在这里插入图片描述

最后

可能有些地方存在错误,欢迎大家指出呀~

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值