zoukankan      html  css  js  c++  java
  • Go学习笔记 由数组和切片 引发的值类型和引用类型的思考

    先看如下代码, 看看运行结果如何:

    func main() {
        var c = [3]int{1, 2, 3}                 //定义一个长度为3的int类型的数组
        d := c                                  //将数组c赋值给d
        d[1] = 100                              //修改数组d中索引为1的值为100
        fmt.Printf("c的值是%v,c的内存地址是%p
    ", c, &c) //c的值是[1 2 3],c的内存地址是0xc42000a180
        fmt.Printf("d的值是%v,d的内存地址是%p
    ", d, &d) //d的值是[1 100 3],d的内存地址是0xc42000a1a0
     
        var a = []int{1, 2, 3, 4, 5}            //creates and array and returns a slice reference
        b := a                                  //此时a,b都指向了内存中的[1 2 3 4 5]的地址
        b[1] = 10                               //相当于修改同一个内存地址,所以a的值也会改变
        fmt.Printf("a的值是%v,a的内存地址是%p
    ", a, &a) //a的值是[1 10 3 4 5],a的内存地址是0xc42000a180
        fmt.Printf("b的值是%v,b的内存地址是%p
    ", b, &b) //b的值是[1 10 3 4 5],b的内存地址是0xc42000a1a0
    }

    运行结果:

    D:GoProjectsrcmain>go run main.go
    c的值是[1 2 3],c的内存地址是0xc000190020
    d的值是[1 100 3],d的内存地址是0xc000190040
    a的值是[1 10 3 4 5],a的内存地址是0xc0001840a0
    b的值是[1 10 3 4 5],b的内存地址是0xc0001840c0

    至于原因 注释已经解释了, c是数组 值类型,a是切片引用类型。 来看看他们忘得的一些介绍吧:

    数组是内置(build-in)类型

    是一组同类型数据的集合,它是值类型,通过从0开始的下标索引访问元素值。在初始化后长度是固定的,无法修改其长度。当作为方法的参数传入时将复制一份数组而不是引用同一指针。数组的长度也是其类型的一部分,通过内置函数len(array)获取其长度。
    注意:和C中的数组相比,又是有一些不同的
    1. Go中的数组是值类型,换句话说,如果你将一个数组赋值给另外一个数组,那么,实际上就是将整个数组拷贝一份
    2.如果Go中的数组作为函数的参数,那么实际传递的参数是一份数组的拷贝,而不是数组的指针。这个和C要区分开。因此,在Go中如果将数组作为函数的参数传递的话,那效率就肯定没有传递指针高了。
    3. array的长度也是Type的一部分,这样就说明[10]int和[20]int是不一样的。

    数组初始化 

    [5] int {1,2,3,4,5}长度为5的数组,其元素值依次为:12345
    [5] int {1,2}长度为5的数组,其元素值依次为:12000
    //在初始化时没有指定初值的元素将会赋值为其元素类型int的默认值0,string的默认值是"" 
    [...] int {1,2,3,4,5}长度为5的数组,其长度是根据初始化时指定的元素个数决定的 
    [5] int { 2:1,3:2,4:3}长度为5的数组,key:value,其元素值依次为:00123。在初始化时指定了2,3,4索引中对应的值:123 
    [...] int {2:1,4:3}长度为5的数组,起元素值依次为:00103。由于指定了最大索引4对应的值3,根据初始化的元素个数确定其长度为5赋值与使用

    内置类型Slices切片(“动态数组")

    与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。切片中有两个概念:一是len长度,二是cap容量,长度是指已经被赋过值的最大下标+1,可通过内置函数len()获得。容量是指切片目前可容纳的最多元素个数,可通过内置函数cap()获得。切片是引用类型,因此在当传递切片时将引用同一指针,修改值将会影响其他的对象。 
    切片可以通过数组来初始化,也可以通过内置函数make()初始化.初始化时len=cap,在追加元素时如果容量cap不足时将按len的2倍扩容 查看示例代码,在线运行示例代码
    切片初始化

    s :=[] int {1,2,3 } 直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3
    s := arr[:] 初始化切片s,是数组arr的引用
    s := arr[startIndex:endIndex] 将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片
    s := arr[startIndex:] 缺省endIndex时将表示一直到arr的最后一个元素
    s := arr[:endIndex] 缺省startIndex时将表示从arr的第一个元素开始
    s1 := s[startIndex:endIndex] 通过切片s初始化切片s1
    s :=make([]int,len,cap) 通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片

    slice可以从一个数组或一个已经存在的slice中再次声明。slice通过array[i:j]来获取,其中i是数组的开始位置,j是结束位置,但不包含array[j],它的长度是j-i

    我们再来看看 值类型 和引用类型的一些例子吧

    值类型

    值类型包括基本数据类型,int,float,bool,string,以及数组和结构体(struct)。值类型变量声明后,不管是否已经赋值,编译器为其分配内存,此时该值存储于栈上。值类型的默认值:

    var a int   //int类型默认值为 0
    var b string    //string类型默认值为 nil空
    var c bool      //bool类型默认值为false
    var d [2]int    //数组默认值为[0 0]
    fmt.Println(&a) //默认已经分配内存地址,可以使用&来取内存地址

    当使用等号=将一个变量的值赋给另一个变量时,如 j = i ,实际上是在内存中将 i 的值进行了拷贝,可以通过 &i 获取变量 i 的内存地址。此时如果修改某个变量的值,不会影响另一个。

    //变量的赋值
    var a =10   //定义变量a
    b := a      //将a的值赋值给b
    b = 101     //修改b的值,此时不会影响a
    fmt.Printf("a的值是%v,a的内存地址是%p
    ",a,&a)   //a的值是10,a的内存地址是0xc42000e228
    fmt.Printf("b的值是%v,b的内存地址是%p
    ",b,&b)   //b的值是101,b的内存地址是0xc42000e250
    //数组的赋值
    var c =[3]int{1,2,3}    //定义一个长度为3的int类型的数组
    d := c      //将数组c赋值给d
    d[1] = 100  //修改数组d中索引为1的值为100
    fmt.Printf("c的值是%v,c的内存地址是%p
    ",c,&c)   //c的值是[1 2 3],c的内存地址是0xc42000a180
    fmt.Printf("d的值是%v,d的内存地址是%p
    ",d,&d)   //d的值是[1 100 3],d的内存地址是0xc42000a1a0

    画图示例:

    引用类型

    引用类型包括指针,slice切片,map ,chan,interface。变量直接存放的就是一个内存地址值,这个地址值指向的空间存的才是值。所以修改其中一个,另外一个也会修改(同一个内存地址)。引用类型必须申请内存才可以使用,make()是给引用类型申请内存空间。

    var a = []int{1,2,3,4,5}
    b := a      //此时a,b都指向了内存中的[1 2 3 4 5]的地址
    b[1] = 10   //相当于修改同一个内存地址,所以a的值也会改变
    c := make([]int,5,5)    //切片的初始化
    copy(c,a)   //将切片acopy到c
    c[1] = 20   //copy是值类型,所以a不会改变
    fmt.Printf("a的值是%v,a的内存地址是%p
    ",a,&a)   //a的值是[1 10 3 4 5],a的内存地址是0xc42000a180
    fmt.Printf("b的值是%v,b的内存地址是%p
    ",b,&b)   //b的值是[1 10 3 4 5],b的内存地址是0xc42000a1a0
    fmt.Printf("c的值是%v,c的内存地址是%p
    ",c,&c)   //c的值是[1 20 3 4 5],c的内存地址是0xc42000a1c0
    d := &a     //将a的内存地址赋值给d,取值用*d
    a[1] = 11
    fmt.Printf("d的值是%v,d的内存地址是%p
    ",*d,d)   //d的值是[1 11 3 4 5],d的内存地址是0xc420084060
    fmt.Printf("a的值是%v,a的内存地址是%p
    ",a,&a)   //a的值是[1 11 3 4 5],a的内存地址是0xc420084060

    a,b,c底层数组是一样的,但是上层切片不同,所以内存地址不一样。

    下面是来之网上的 《关于 Go 中 Map 类型和 Slice 类型的传递》内容

    Map 类型

    先看例子 m1:

    func main() {
        m := make(map[int]int)
        mdMap(m)
        fmt.Println(m)
    }
     
    func mdMap(m map[int]int) {
        m[1] = 100
        m[2] = 200
    }

    结果是  map[2:200 1:100]

    我们再修改如下 m2:

    func main() {
        var m map[int]int
        mdMap(m)
        fmt.Println(m)
    }
     
    func mdMap(m map[int]int) {
        m = make(map[int]int)
        m[1] = 100
        m[2] = 200
    }

    发现结果变成了 map[]

    要理解这个问题,需要明确在 Go 中不存在引用传递,所有的参数传递都是值传递。

    现在再来分析下,如图:

    可能有些人会有疑问,为什么途中的 m 像是一个指针呢。查看官方的 Blog 中有写:

    Map types are reference types, like pointers or slices, ...

    这边说 Map 类型是引用类型,像是指针或是 Slice(切片)。所以我们基本上可以把它当作是指针来看待(注意,只是近似,或者说其中含有指针,其内部仍然含有其他信息,这里只是为了便于理解),只不过这个指针有些特殊罢了。

    m1 中,当调用 mdMap 方法时重新开辟了内存,将 m 的内容,也就是 map 的地址拷贝入了 m',所以此时当操作 map 时,m 和 m' 所指向的内存为同一块,就导致 m 的 map 发生了改变。

    而在 m2 中,在调用 mdMap 之前,m 并未分配内存,也就是说并未指向任何的 map 内存区域。从未导致 m' 的 map 修改不能反馈到 m 上。

    Slice 类型

    现在看一下 Slice。

    s1:

    func main() {
        s := make([]int, 2)
        mdSlice(s)
        fmt.Println(s)
    }
     
    func mdSlice(s []int) {
        s[0] = 1
        s[1] = 2
    }

    s2:

    func main() {
        var s []int
        mdSlice(s)
        fmt.Println(s)
    }
     
    func mdSlice(s []int) {
        s = make([]int, 2)
        s[0] = 1
        s[1] = 2
    }

    不出所料:

    s1 结果为 [1 2]

    s2 为 []

    因为正如官方所说,Slice 类型与 Map 类型一样,类似于指针,Slice 中仍然含有长度等信息。

    修改一下 s1,变成 s3:

    func main() {
        s := make([]int, 2)
        mdSlice(s)
        fmt.Println(s)
    }
     
    func mdSlice(s []int) {
        s = append(s, 1)
        s = append(s, 2)
    }

    不再修改 slice 原先的两个元素,而加上另外两个,结果为:[0 0]

    发现修改并没有反馈到原先的 slice 上。

    这里我们需要把 slice 想象为特殊的指针,其已经保存了所指向内存区域长度,所以 append 之后的内存并不会反映到 main() 中,当新元素通过调用 append 函数追加到切片末尾时,如果超出了容量,append 内部会创建一个新的数组。并将原有数组的元素被拷贝给这个新的数组,最后返回建立在这个新数组上的切片。这个新切片的容量是旧切片的二倍(译者注:当超出切片的容量时,append 将会在其内部创建新的数组,该数组的大小是原切片容量的 2 倍。最后 append 返回这个数组的全切片,即从 0 到 length - 1 的切片):

    那如何才能反映到 main() 中呢?没错,使用指向 Slice 的指针。

    func mdSlice(s *[]int) {
        *s = append(*s, 1)
        *s = append(*s, 2)
    }

    内存如图所示:

    注意本文中内存区域分配是否连续完全随机,不影响程序,只是为了图解清晰。

    Chan 类型

    Go 中 make 函数能创建的数据类型就 3 类:Slice, Map, Chan。不比多说,相比读者已经能想象 Chan 类型的内存模型了。的确如此,读者可以自己尝试,这边就不过多赘述了。(可以通通过 == nil 的比较来进行测试)。

    参考:

    https://www.cnblogs.com/itbsl/p/10599948.html

    https://www.cnblogs.com/liuzhongchao/p/9159896.html

    https://blog.csdn.net/belalds/article/details/80076739

    https://www.cnblogs.com/snowInPluto/p/7477365.html

  • 相关阅读:
    C#构造函数
    C#析构函数
    C#常量
    C#属性
    checklistbox的用法
    2012快捷键
    查询ORACLE存储关联表
    UltraDropDown
    Linux常用命令大全(非常全!!!)
    infra 仪表盘效果
  • 原文地址:https://www.cnblogs.com/majiang/p/14209985.html
Copyright © 2011-2022 走看看