切片
为什么需要切片
- 用于元素的个数不确定,所以无法通过数组的形式来进行统计。此时就需要切片
切片,也因此可以粗略地理解为动态数组 - 数组的长度不能用变量来确定,这时候切片slice也就派上用场了
切片地基本介绍
- 切片的英文是slice
- 切片是数组的一种引用,这也说明切片是引用类型,在传递时,遵守引用传递的机制
- 切片的使用和数组类似,遍历、求长、访问都是一样的
- 切片的长度是可变的,因此说切片可以粗略理解为动态数组
- 切片定义的语法:
var 切片名 []类型
var a []int
切片的快速入门
var arr [5]int = [5]int{1, 2, 45, 6, 3}
a := arr[2:4]
fmt.Println(arr)
fmt.Println(a)
fmt.Printf(“a的长度%d\n”, len(a))
fmt.Printf(“a的容量%d\n”, cap(a))
fmt.Printf(“a指向的地址%v\n”, &a[0])
输出结果为:
[1 2 45 6 3]
[45 6]
a的长度2
a的容量3
a指向的地址0xc000010240
可见,a := arr[2:4],这段代码就是将arr数组的下标为2-3的元素。
其中cap()函数是用来求切片容量的,容量是指切片目前最多能存储的元素个数,并且这个容量是可以动态更改的
切片在内存中的形式
- slice在内存中有三个值,一个是slice所指向的第一个元素的地址,一个是这个slice的长度,还有一个是这个slice的容量
- 根据这第一个值,我们也很容易判断出slice是引用类型
- 其实,我们可以把slice看成一种结构体struct。其由这三个值组成。
type slice struct {
array *[n]T // 指向底层数组的指针
len int // 当前长度
cap int // 总容量
} - 另外,值得注意的一点是,由于slice是引用类型,所以当用slice[1]来修改值时,其所指的地址的值发送改变,也就是原数组的内容也会发送改变
代码用例:
var arr [5]int = [5]int{1, 2, 45, 6, 3}
a := arr[2:4]
fmt.Println(arr)
fmt.Println(a)
a[0] = 100
fmt.Println(arr)
fmt.Println(a)
输出结果为:
[1 2 45 6 3]
[45 6]
[1 2 100 6 3]
[100 6]
切片的使用
- 定义一个切片,让切片去引用一个定义好的数组
- 用make来创建切片
基本语法:var 切片名 []类型 = make([]类型,切片长度,切片容量)
要求:容量必须大于等于长度
创建完的切片里的元素都是默认值
这样定义出来的切片所指向的数组,是对外隐蔽的,只能通过slice来访问这个数组
- 定义切片的时候,直接指定具体的数组
代码示例:
func main() {
var arr []int = []int{1, 2, 41, 21}
fmt.Printf(“%v\n”, len(arr))
fmt.Printf(“%v”, arr)
} - 方式一和方式二的区别:
方式一:直接引用数组,是要求先定义好了一个数组,这个数组是可见的
方式二:用make创建切片的同时,也会创建数组,这个数组是由切片在底层进行维护的数组,是不可见的,只能通过slice来对它进行访问修改等
切片的遍历
- 传统for循环
- for-range语句遍历
func main() {
var n [5]int = [5]int{2, 12, 31, 12, 45}
slice1 := n[1:3]
for i := 0; i < len(slice1); i++ {
fmt.Printf("%v “, slice1[i])
}
fmt.Println()
for index, value := range slice1 {
fmt.Printf(”%v,%v ", index, value)
}
}
切片的使用细节和注意事项
- 切片初始化时,var slice = arr[start:end]
其中切片slice是从arr数组的start下标开始,到end下标结束,但是不将end下标的元素也囊括在内
- 在使用切片时,也只能按照0-len(slice)-1的范围来,不能够直接来越界初始化,需要先动态增长先
- slice :=arr[下标1:下标2]
- 如果不写下标1,默认是0,从头开始
- 如果不写下标2,默认是end,把最后一位元素也算进去
- 所以要想表示切片获取整个数组,有如下几种简写
- slice :=arr[:]
- slice :=arr[0:]
- slice :=arr[:len(arr)]
- cap是一个内置函数,用于计算切片的容量,也就是最大可以存放多少元素
- 切片定义完后,本身还是空的,需要让其引用数组,或者用make函数创建一个空间供切片使用
- 切片可以继续切片
- append函数
- append的用法
- slice = append(slice, elem1, elem2, …)
给slice切片添加后面几个元素,这里的…表示的是省略
- slice = append(slice, anotherSlice…)
在slice后面接收另一个切片,这里的…是不能省略的,表示将后面的切片各个元素拆开加入
不论append的哪种用法,都必须要有接收,不然相当于没有任何作用,因为append函数并不会去改变原来的切片 - 扩不扩容
- 当长度在append后>容量时才会扩容,否则不会
2. 只有在扩容时,才会执行下面的(45678),因为如果不用扩容,就可以直接在原来的切片上更改,不需要额外创建新数组(拥有足够大的容量)
- 当长度在append后>容量时才会扩容,否则不会
- append函数本质就是对数组的扩容
- append函数会创建一个新的数组newarr(是扩容后的大小)
- 将slice原来的元素拷贝到newarr
- 在newarr上添加新元素
- 返回新的切片(指向新数组),比如:slice重新引用到newarr
- newarr是底层维护的,程序员不可见
- append的用法
- 切片的拷贝
copy函数
语法copy(para1,para2)
para1,para2这两个都需要是切片,并且是将para2的内容给到para1里面
此处不在乎二者的长度的大小关系,反正就是逐位去拷贝
拷贝并不会使这两个切片的空间有什么关联,依然是相互独立的
- 切片是引用类型,所以传递时遵循引用传递,向函数传递切片时,函数内部对切片的操作,也会对外部操作有影响
代码示例:
func test(a []int) {
a[0] = 100
}
func main() {
var a []int = make([]int, 4, 8)
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
fmt.Println(a)
test(a)
fmt.Println(a)
}
输出结果:
[1 2 3 4]
[100 2 3 4]
string和slice的关系
- string的底层是一个byte数组,因此string也可以用切片来处理
- string的核心结构:
type stringStruct struct {
str unsafe.Pointer // 指向底层字节数组的指针
len int // 字符串的字节长度(不是字符数)
}
这和切片的结构是很像的,只是没有容量,因为数组的容量大小就是长度大小,因为它长度不可变 - string本身是不可变的,不能通过str[0]="1"这种类似形式去修改(和java一样)
- 如果要修改string的值,需要先将string转换成[]byte或者[]rune,修改完后,再转成string
有中文的情况转成[]rune,因为[]rune是按照字符,一个中文占三四个字节