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

  • 相关阅读:
    字符串 CSV解析 表格 逗号分隔值 通讯录 电话簿 MD
    Context Application 使用总结 MD
    RxJava RxPermissions 动态权限 简介 原理 案例 MD
    Luban 鲁班 图片压缩 MD
    FileProvider N 7.0 升级 安装APK 选择文件 拍照 临时权限 MD
    组件化 得到 DDComponent JIMU 模块 插件 MD
    gradlew 命令行 build 调试 构建错误 Manifest merger failed MD
    protobuf Protocol Buffers 简介 案例 MD
    ORM数据库框架 SQLite 常用数据库框架比较 MD
    [工具配置]requirejs 多页面,多入口js文件打包总结
  • 原文地址:https://www.cnblogs.com/majiang/p/14209985.html
Copyright © 2011-2022 走看看