文章目录
导言
在下面,我会先说说切片和数组的关系,之后再提及切片的追加、切片的拷贝和切片作为参数的一些注意点,至于切片元素的删除等基本操作,本文不涉及,想要了解的话可以看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 字节
。当然,除了这三个属性,切片还有一个地址,用以表示切片在内存中的位置,我简称它为切片内存地址。
注意:切片指向地址和切片内存地址是不一样的! -
数组的结构
数组在内存中的表现为一块连续的内存空间,该内存空间的大小等于数组元素个数 * 每个元素所占字节数
,比如一个长度为10
的int32
的数组,其所占用的内存空间大小为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
中对应的元素(等等在下部分验证)。
二者在内存中的关系如下:
![](https://img-blog.csdnimg.cn/20190906160107903.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzE5MDE4Mjc3,size_16,color_FFFFFF)
以上是切片指向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
函数,两个切片所指向的内存地址是不一样的。所以slice3
和slice4
是独立的,二者不会相互影响。我们称这种拷贝为深拷贝。 (其实使用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]
}
如果你看完了这一篇,做了这个题,欢迎把答案在评论区打出喔~
总结
最后
可能有些地方存在错误,欢迎大家指出呀~