当前位置:网站首页>go 语言 数组,字符串,切片

go 语言 数组,字符串,切片

2022-04-23 13:59:00 面试被拒1万次

数组,字符串,切片

笔记内容 是个人总结《Go语言高级编程》的学习笔记

基础的数据结构,只有在这些不满足的时候才会使用链表,map,结构体等数据结构。

数组,字符串,切片 有相同的内存结构,都是连续的内存块,因为在语法上的限制才有不同的表现,了解内存结构,可以更好的理解和使用它们

  • 数组: 对应的连续的字节数组,可以修改内存数据,但是它是值类型,赋值和传参都是整体赋值
  • 字符串:对应的连续字节数组,不可以修改内存数据,只读属性
  • 切片: 用的最多,同样的连续字节数组,但是切片头部包含底层数据的指针,数据长度和容量信息,传参时候只需要传递这些信息,更加高效

数组

连续内存的数据类型的内存块,跟c语言不同的是,Go中的数组名是一个值类型,

  • 在c语言中,数组名表示数组的首元素地址,也就是说数组名具有地址的概念
  int a[10],*p;
  p = a;
  p = &a[0]; //做参数传递的时候,只是传递首地址
  • 在go中,数组名表示的就是一个数组变量,并不是指向数组第一个元素的地址,在赋值或者传参的时候,传递的是整个数组,不够高效
   a := [...]int {
    1,2,3}  //声明并初始化 a = {1,2,3}
   func test(a [3]int){
    
      ...
   }
   test(a) //传递是整个数组

数组可以定义多种数组类型,看着挺好玩的

  • 字符数组
  var s1=[2]string{
    "hello", "world"}
  • 结构体数组
  var line2 = [...]People.Student{
    People.Student{
    age : 30 , score :100}, People.Student{
    age : 20 , score :100}}
  • 函数数组
var decoder2 = [...] func (io.Reader) (image.Image, error){
    
                              png.Decode,
                              jpeg.Decode,
                            }
  • 接口数组
  var unknown2 = [...] interface {
    }{
     123 , "你好" }
  • 管道数组
  var chanList = [2] chan int{
    }

字符串

字符串是不可改变 字符序列,Go语言中字符串的定义是一个结构体

  type  StringHeader  struct  {
    
      Data  uintptr
      Len int
    }

个人理解: 在栈上开辟一个结构体内存,Data 指向静态区的字符常量,Len表示字符长度,也就是每个字符串的底层结构也是一个结构体,书中也说字符数组本质也是结构体数组

迷惑未解:

func testString() {
    
    string1 := [...]string{
    "helloooooooo", "worlddddddd"}
    fmt.Printf("arr = %p, string1[0] =%p,string1[0] =%s,len string1[0] = %d,string[1] = %p", &string1, &string1[0], string1[0], len(string1[0]), &string1[1])

    //arr = 0xc000062040, string1[0] =0xc000062040,string1[0] =helloooooooo,len string1[0] = 12,string[1] = 0xc000062050
}
// string1[0] 的长度是12,
// &string1[1] - &string[0] = 10 为什么会这样,我认为的结果会是 &string1[1] - &string[0] = 13

切片

出自Go语言高级编程

切片的结构定义

type  SliceHeader  struct  {
    
    Data  uintptr
    Len   int
    Cap   int
  }

切片是一种简单的动态数组,比字符串多了一个cap容量,动态数组的思想就是,只要长度小于容量,添加元素的时候,不大于容量就不会重新分配内存,不会做数据拷贝,超过容量会重新分配内存空间,然后切片拷贝。在赋值和传参的时候,都是结构体的赋值和传参

切片定义

   var a = []int   // nil切片 a == nil
   var b = []int {
     } //空集合 ,不等于 nil
   var c = []int {
    1,2,3}  //len = 3 ,cap = 3
   var d = make([]int ,2,3) //len = 2 , cap = 3

数组的区别

  • 相同点

内置的 len 函数返回切片中有效元素的长度, 内置的 cap 函数返回切片容量大小,遍历和计算长度都一样

  • 不同点
  1. 切片容量大于或等于长度,数组一定等于.
  2. 有长度定义的是数组,没有的是切片。
  3. 数组是值类型,赋值是整体赋值,切片是结构体赋值,数组的类型要长度跟数据类型都一样,切片的类型与长度无关,只要相同的数据类型构成的切片都是同类型的切片,

切片的增加和删除

首先看下切片的长度和容量在赋值的时候的计算


func SliceAssignment() {
    

	//首先回顾一下 什么是开闭区间
	// a,b 之间的所有实数, 但不包括 a,b 记做(a,b)
	// a,b 之间的所有实数, 包括a,b 且 a < b 记做[a,b]

	// 切片的开闭规则
	a := []int{
    10, 20, 30, 40, 50, 60, 7, 8, 9}
	fmt.Printf(" len: %d, cap: %d , arr = %p , arr %+v \n", len(a), cap(a), a, a)
	//len: 9, cap: 9 , arr = 0xc0000ba000 , arr [10 20 30 40 50 60 7 8 9]

	d := a[:3] // 左闭右开 [0,3)
	fmt.Printf(" len: %d, cap: %d , arr = %p , arr %+v \n", len(d), cap(d), d, d)
	//len: 3, cap: 9 , arr = 0xc0000ba000 , arr [10 20 30]

	b := a[2:]
	fmt.Printf(" len: %d, cap: %d , arr = %p , arr %+v \n", len(b), cap(b), b, b)
	// len: 7, cap: 7 , arr = 0xc0000ba010 , arr [30 40 50 60 7 8 9]

	c := a[2:6]
	fmt.Printf(" len: %d, cap: %d , arr = %p , arr %+v \n", len(c), cap(c), c, c)
	//len: 4, cap: 7 , arr = 0xc0000ba010 , arr [30 40 50 60]

}

总结

如何计算切片的长度和 容量
newslice := slice[ i : j ]
不写的情况下 默认 i = 0 , j = len(slice) k= cap(slice)
len(newslice) = j - i example: len(d) = 3 - 0 , len = 6-2
cap(newslice) = k - i example: len(d) = 9 -0 , len(b) = len = 9 - 2

追加元素 append()

a := []int{
    1, 2, 3}

//在开头添加1个切片
a = append([]int{
    100, 200, 300}, a...)
fmt.Printf("a= len: %d, cap: %d , arr = %p , arr %+v \n", len(a), cap(a), a, a)
//a= len: 6, cap: 6 , arr = 0xc000196030 , arr [100 200 300 1 2 3]

// 在i=3 的位置添加切片,重新分配了内存空间
a = append(a[:3], append([]int{
    1000, 2000}, a[3:]...)...)
fmt.Printf("a= len: %d, cap: %d , arr = %p , arr %+v \n", len(a), cap(a), a, a)
// a= len: 8, cap: 12 , arr = 0xc0001ae000 , arr [100 200 300 1000 2000 1 2 3]


b := append([]int{
    666, 777}, a[4:]...) //重新解了包
fmt.Printf("b= len: %d, cap: %d , arr = %p , arr %+v \n", len(b), cap(b), b, b)
//b= len: 6, cap: 6 , arr = 0xc000196090 , arr [666 777 2000 1 2 3]

// a 并没有改变
fmt.Printf("a= len: %d, cap: %d , arr = %p , arr %+v \n", len(a), cap(a), a, a)
//a= len: 8, cap: 12 , arr = 0xc0001ae000 , arr [100 200 300 1000 2000 1 2 3]

// a改变了,a和c 指向同一块内存空间
c := append(a[:3], []int{
    888, 999}...)
fmt.Printf("a= len: %d, cap: %d , arr = %p , arr %+v \n", len(a), cap(a), a, a)
//a= len: 8, cap: 12 , arr = 0xc0001ae000 , arr [100 200 300 888 999 1 2 3]
fmt.Printf("c= len: %d, cap: %d , arr = %p , arr %+v \n", len(c), cap(c), c, c)
//c= len: 5, cap: 12 , arr = 0xc0001ae000 , arr [100 200 300 888 999]

总结

在头插入一般都会重新分配空间,有数组的拷贝。在切片中间,或者尾部插入都会创建一个临时切片,将a[i:]的内容复制进去,然后追加到a[:i]

不创建临时切片的插入切片:
copy(des,src) 函数

func testcopy() {
    
    s1 := []int{
    1, 2, 3}
    s2 := []int{
    4, 5}
    s3 := []int{
    6, 7, 8, 9}
    // copy(s1, s2)
    // fmt.Println(s1) //[4 5 3]
    copy(s2, s1)
    fmt.Println(s2) //[1.2]
    copy(s2, s3)
    fmt.Println(s2) //[6 7]
}

使用copy()函数,不创建临时变量

copy(a[4:], a[3:]) // a[i:]向后移动1个位置,长度不变,尾部元素去掉一个
fmt.Printf(" len: %d, cap: %d , arr = %p , arr %+v \n", len(a), cap(a), a, a)
//len: 8, cap: 12 , arr = 0xc0001ae000 , arr [100 200 300 888 888 999 1 2]

//如果不创建临时变量
i := 2
x := []int{
    101, 102, 103}
a = append(a, x...) //为x切片扩展足够的空间
copy(a[i+len(x):], a[i:]) // a[i:]向后移动len(x)个位置
copy(a[i:], x)   // 复制新添加的切片

切片的删除
普通删除

  var a = []int {
    1,2,3}
  a = a[ :len(a) - 1]  //删除尾部一个元素
  a = a[ :len(a) - N]  // 删除尾部 N个元素

  a = a[1:] // 删除头部1个元素
  a = a[N:] // 删除头部N个元素

使用append原地完成:所谓原地完成是指在原 有的切片数据对应的内存区间内完成,不会导致内存空间结构 的变化

func removeSlice() {
    

	var a = []int{
    1, 2, 3, 4, 5}
	fmt.Printf("removeSlice 原地址= len: %d, cap: %d , arr = %p , arr %+v \n", len(a), cap(a), a, a)
  //removeSlice 原地址= len: 5, cap: 5 , arr = 0xc0000aa0f0 , arr [1 2 3 4 5]

	a = a[:len(a)-1]
	fmt.Printf("removeSlice 删除尾部= len: %d, cap: %d , arr = %p , arr %+v \n", len(a), cap(a), a, a)
  //removeSlice 删除尾部= len: 4, cap: 5 , arr = 0xc0000aa0f0 , arr [1 2 3 4]

	a = a[1:]
	fmt.Printf("removeSlice 删除头部= len: %d, cap: %d , arr = %p , arr %+v \n", len(a), cap(a), a, a)
  //removeSlice 删除头部= len: 3, cap: 4 , arr = 0xc0000aa0f8 , arr [2 3 4]

	a = append(a[:0], a[1:]...)
	fmt.Printf("removeSlice 删除头部= len: %d, cap: %d , arr = %p , arr %+v \n", len(a), cap(a), a, a)
  //removeSlice 删除头部= len: 2, cap: 4 , arr = 0xc0000aa0f8 , arr [3 4]
}

总结
普通删除会有地址变化,数据移动,容量变小,使用append 不会用有位置变化,容量也不会变化(这里是为什么,还没搞清楚),使用append效率更高

所以删除元素

  a = append(a[:i],a[i+1:]...)
  a = append(a[:i],a[i+N:]...)

切片的高效操作:

  • 要降低内存分配的次数,尽量保证 append 操作不会超出 cap 的容量,降低触发内存分配的次
    数和每次分配内存大小。
  • 避免内存泄漏,虽然有自动回收机制,但是在操作指针的时候,还是应该优化代码,告诉编译器尽早自动回收需要释放的内存空间,
func SliceDel() {
    
  	a := []*int{
    }
  	b := 100
  	c := 10
  	a = append(a, &b, &c)
  	a[len(a)-1] = nil // 使用指针对象的 切片,最好还是手动设置为nil,保证自动回收器可以发现需要回收的对象
  	a = a[:len(a)-1]
}
  • 切片类型强制转化。为了安全,当两个切片类型 []T 和 []Y 的底层原始切片类 型不同时,Go语言是无法直接转换类型的。不过安全都是有 一定代价的,有时候这种转换是有它的价值的——可以简化编 码或者是提升代码的性能。

这篇读书笔记,读《GO语言高级编程》所得,大家可以购买正版书籍,还是很有价值的。

版权声明
本文为[面试被拒1万次]所创,转载请带上原文链接,感谢
https://blog.csdn.net/m0_38023160/article/details/122297719