zoukankan      html  css  js  c++  java
  • 第十五章:指针类型

    image

    本篇翻译自《Practical Go Lessons》 Chapter 15: Pointer type

    1 你将在本章将学到什么?

    • 什么是指针?
    • 什么时指针类型?
    • 如何去创建并使用一个指针类型的变量。
    • 指正类型变量的零值是什么?
    • 什么是解除引用?
    • slices, maps, 和 channels 有什么特殊的地方?

    2 涵盖的技术概念

    • 指针
    • 内存地址
    • 指针类型
    • 解除引用
    • 引用

    3 什么是指针?

    指针是“是一个数据项,它存储另外一个数据项的位置”。
    在程序中,我们不断地存储和检索数据。例如,字符串、数字、复杂结构…。在物理层面,数据存储在内存中的特定地址,而指针存储的就是这些特定内存地址。

    image

    记住指针变量,就像其他变量一样,它也有一个内存地址。

    4 指针类型

    Go 中的指针类型不止一种,每一种普通类型就对应一个指针类型。相应地,指针类型也限定了它自己只能指向对应类型的普通变量(地址)。

    指针类型的语法为:

    *BaseType
    

    BaseType指代的是任何普通类型。

    我们来看一下例子:

    • *int 表示指向 int 类型的指针
    • *uint8 表示指向 uint8 类型的指针
    type User struct {
    	ID string
    	Username string
    }
    
    • *User 表示指向 User 类型的指针

    5 如何去创建一个指针类型变量?

    下面的语法可以创建:

    var p *int
    

    这里我们创建了一个类型为 *int 的变量 p*int 是指针类型(基础类型是 int)。

    让我们来创建一个名为 answer 的整型变量。

    var answer int = 42
    

    现在我们给变量 p 分配一个值了:

    p = &answer
    

    使用 & 符号我们就能得到变 answer地址。来打印出这个地址~

    fmt.Println(p)
    // 0xc000012070
    

    0xc000012070 是一个十六进制数字,因为它的以 0x 为前缀。内存地址通常是以十六进制格式表示。你也可以使用二进制(用 0 和 1)表示,但不易读。

    6 指针类型的零值

    指针类型的零值都是 nil,也就是说,一个没有存储地址的指针等于 nil

    var q *int
    fmt.Println(q == nil)
    // true
    

    7 解除引用

    一个指针变量持有另一个变量的地址。如果你想通过指针去访问地址背后的变量值该怎么办?你可以使用解除引用操作符 *

    来举个例子,我们定义一个结构体类型 Cart

    type Cart struct {
    	ID string
    	Paid bool
    }
    

    然后我们创建一个 Cart 类型的变量 cart,我们可以得到这个变量的地址,也可以通过地址找到这个变量:
    image
    image

    • 使用 * 操作符,你可以通过地址找到变量值
    • 使用 & 操作符,你可以得到变量的地址

    7.1 空指针解引用:运行时 panic

    每个 Go 程序员都会遇到这个 panic(报错):

    panic: runtime error: invalid memory address or nil pointer dereference
    [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1091507]
    

    为了更好地理解它,我们来复现一下:

    package main
    
    import "fmt"
    
    func main() {
        var myPointerVar *int
        fmt.Println(*myPointerVar)
    }
    

    在程序里,我们的定义了一个指针变量 myPointerVar,这个变量的类型是 *int(指向整型)。

    然后我尝试对它进行解引用,myPointerVar 变量持有一个尚未初始化的指针,因此该指针的值为 nil。因为我们尝试去寻找一个不存在的地址,程序将会报错!我们尝试找到空地址,而空地址在内存中不存在。

    8 Maps 和 channels

    Maps 和 channels 变量里保存了对内部结构的指针。因此,即便向一个函数或方法传递的 map 或 channel 不是指针类型,也开始对这个 map 或 channel 进行修改。让我们看一个例子:

    func addElement(cities map[string]string) {
        cities["France"] = "Paris"
    }
    
    • 这个函数将一个 map 作为输入
    • 它向 map 中添加一项数据(key = "France", value = "Paris")
    package main
    
    import "log"
    
    func main() {
        cities := make(map[string]string)
        addElement(cities)
        log.Println(cities)
    }
    
    • 我们初始化一个名为 cities 的 map
    • 然后调用函数 addElement
    • 程序打印出:
    map[France:Paris]
    

    我们将在专门的部分中更广泛地介绍 channels 和 maps。

    9 切片

    9.1 切片定义

    切片是相同类型元素的集合。在内部,切片是一个具有三个字段的结构:

    • length:长度
    • capacity:容量
    • pointer:执向内部数组的指针
      下面是一个关于切片 EUcountries 的例子:
    package main
    
    import "log"
    
    func main() {
        EUcountries := []string{"Austria", "Belgium", "Bulgaria"}
        log.Println(EUcountries)
    }
    

    9.2 函数或方法将切片作为参数或接收器:小心

    9.2.0.1 Example1: 向切片添加元素

    package main
    
    import "log"
    
    func main() {
        EUcountries := []string{"Austria", "Belgium", "Bulgaria"}
        addCountries(EUcountries)
        log.Println(EUcountries)
    }
    
    func addCountries(countries []string) {
        countries = append(countries, []string{"Croatia", "Republic of Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", "Luxembourg", "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Sweden"}...)
    }
    
    • 函数 addCountries 将一个字符串类型切片作为参数
    • 它通过内建函数 append 向切片添加字符串来修改切片
    • 它将缺失的欧盟国家附加到切片中
      问题:依你看,程序的输出将会是下面的哪个?
    [Austria Belgium Bulgaria Croatia Republic of Cyprus Czech Republic Denmark Estonia Finland France Germany Greece Hungary Ireland Italy Latvia Lithuania Luxembourg Malta Netherlands Poland Portugal Romania Slovakia Slovenia Spain Sweden]
    
    [Austria Belgium Bulgaria]
    

    答案:这个函数实际输出:

    [Austria Belgium Bulgaria]
    

    9.2.0.2 解释

    • 这个函数将[]string类型元素作为参数
    • 当函数被调用时,Go 会将切片 EUcountries 拷贝一份传进去
    • 函数将得到一个拷贝的切片数据:
      • 长度
      • 容量
      • 指向底层数据的指针
    • 在函数内部,缺失的国家被添加了进去
    • 切片的长度会增加
    • 运行时将分配一个新的内部数组

    让我们在函数中添加一个日志来可视化它:

    func addCountries(countries []string) {
        countries = append(countries, []string{"Croatia", "Republic of Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", "Luxembourg", "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Sweden"}...)
        log.Println(countries)
    }
    

    日志打印出:

    [Austria Belgium Bulgaria Croatia Republic of Cyprus Czech Republic Denmark Estonia Finland France Germany Greece Hungary Ireland Italy Latvia Lithuania Luxembourg Malta Netherlands Poland Portugal Romania Slovakia Slovenia Spain Sweden]
    
    • 这里的改变只会影响拷贝的版本

    9.2.0.3 Example2:更新元素

    package main
    
    import (
        "log"
        "strings"
    )
    
    func main() {
        EUcountries := []string{"Austria", "Belgium", "Bulgaria"}
        upper(EUcountries)
        log.Println(EUcountries)
    }
    
    func upper(countries []string) {
        for k, _ := range countries {
            countries[k] = strings.ToUpper(countries[k])
        }
    }
    
    • 我们添加新函数 upper,它将把一个字符串切片的每个元素都转换成大写

    问题:依你看,程序将传输下面哪个?

    [AUSTRIA BELGIUM BULGARIA]
    
    [Austria Belgium Bulgaria]
    

    答案:这个函数将返回:

    [AUSTRIA BELGIUM BULGARIA]
    

    9.2.0.4 解释

    • 函数 upper 获取切片 EUcountries 的副本(和上面一样)
    • 在函数内部,我们更改切片元素的值 countries[k] = strings.ToUpper(countries[k])
    • 切片副本仍然有对底层数组的引用
    • 我们可以修改!
    • .. 但只有已经在切片中的切片元素。

    9.2.0.5 结论

    • 当你将切片传递给函数时,它会获取切片的副本。
    • 这并不意味着你不能修改切片。
    • 你只可以修改切片中已经存在的元素。

    9.3 函数或方法将切片指针作为参数或接收器

    如果使用切片指针,你就可以在函数中修改这个切片了:

    package main
    
    import (
        "log"
    )
    
    func main() {
        EUcountries := []string{"Austria", "Belgium", "Bulgaria"}
        addCountries2(&EUcountries)
        log.Println(EUcountries)
    }
    
    func addCountries2(countriesPtr *[]string) {
        *countriesPtr = append(*countriesPtr, []string{"Croatia", "Republic of Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", "Luxembourg", "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Sweden"}...)
    }
    

    这个程序将输出:

    [Austria Belgium Bulgaria Croatia Republic of Cyprus Czech Republic Denmark Estonia Finland France Germany Greece Hungary Ireland Italy Latvia Lithuania Luxembourg Malta Netherlands Poland Portugal Romania Slovakia Slovenia Spain Sweden]
    
    • 函数 addCountries2 将字符串切片的指针([]string)作为参数
    • 函数 append 调用时的第一个参数是 *countriesPtr(即我们通过指针 countriesPtr 去找到原值)
    • append 的第二个参数没有改变
    • 函数 addCountries2 的结果会影响到外部的变量

    10 指向结构体的指针

    有一个快捷方式可以让你直接修改 struct 类型的变量而无需使用*运算符:

    type Item struct {
    	SKU string
    	Quantity int
    }
    
    type Cart struct {
    	ID string
    	CreatedDate time.Time
    	Items Item
    }
    
    cart := Cart{
        ID:          "115552221",
        CreatedDate: time.Now(),
    }
    cartPtr := &cart
    cartPtr.Items = []Item{
        {SKU: "154550", Quantity: 12},
        {SKU: "DTY8755", Quantity: 1},
    }
    log.Println(cart.Items)
    // [{154550 12} {DTY8755 1}]
    
    • cart 是一个 Cart 类型变量
    • cartPtr := &cart 会获取变量 cart 的地址然后将其存储到 cartPtr
    • 使用变量 cartPtr,我们可以直接修改变量 cartItem 字段
    • 这是因为运行时自动通过结构体指针找到了原值进行了修改,以下是等价的写法
    (*carPtr).Items = []Item{
        {SKU: "154550", Quantity: 12},
        {SKU: "DTY8755", Quantity: 1},
    }
    

    (这也有效,但更冗长)

    11 使用指针作为方法的接收器

    指针通常用作方法的接收器,让我们以 Cat 类型为例:

    type Cat struct {
      Color string
      Age uint8
      Name string
    }
    

    你可以定义一个方法,使用指向 Cat 的指针作为方法的接收器(*Cat):

    func (cat *Cat) Meow(){
      fmt.Println("Meooooow")
    }
    

    Meow 方法没有做任何有实际意义的事吗;它只是打印了字符串"Meooooow"。我们没有修改比变量的值。我们来看另一个方法,它修改了 cat 的 Name

    func (cat *Cat) Rename(newName string){
      cat.Name = newName
    }
    

    此方法将更改猫的名称。通过指针,我们修改了 Cat 结构体的一个字段。

    当然,如果你不想使用指针作为接收器,你也可以:

    func (cat Cat) RenameV2(newName string){
      cat.Name = newName
    }
    

    在这个例子中,变量 cat 是一个副本。接收器被命名为“值接收器”。因此,你对 cat 变量所做的任何修改都将在 cat 副本上完成:

    package main
    
    import "fmt"
    
    type Cat struct {
        Color string
        Age   uint8
        Name  string
    }
    
    func (cat *Cat) Meow() {
        fmt.Println("Meooooow")
    }
    
    func (cat *Cat) Rename(newName string) {
        cat.Name = newName
    }
    
    func (cat Cat) RenameV2(newName string) {
        cat.Name = newName
    }
    
    func main() {
        cat := Cat{Color: "blue", Age: 8, Name: "Milow"}
        cat.Rename("Bob")
        fmt.Println(cat.Name)
        // Bob
    
        cat.RenameV2("Ben")
        fmt.Println(cat.Name)
        // Bob
    }
    

    在主函数的第一行,我们创建了一个 Cat 类型的变量 cat,它的 Name 是 "Millow"
    当我们调用具有值接收器RenameV2 方法时,函数外部变量 cat 的 Name 没有发生改变。
    当我们调用 Rename 方法时,cat 的 Name 字段值会发生变化。
    image

    11.1 何时使用指针接收器,何时使用值接收器

    • 以下情况使用指针接收器:
      • 你的结构体很大(如果使用值接收器,Go 会复制它)
      • 你想修改接收器(例如,你想更改结构变量的名称字段)
      • 你的结构包含一个同步原语(如sync.Mutex)字段。如果你使用值接收器,它还会复制互斥锁,使其无用并导致同步错误。
      • 当接收器是一个 map、func、chan、slice、string 或 interface值时(因为在内部它已经是一个指针)
      • 当你的接收器是持有指针时

    12 随堂测试

    12.1 问题

    1. 如何去表示一个持有指向 Product 指针的变量?
    2. 指针类型的零值是多少?
    3. "解引用(dereferencing)" 是什么意思?
    4. 如何解引用一个指针?
    5. 填空: ____ 在内部是一个指向 ____ 的指针。
    6. 判断正误:当我想函数中修改 map 时,我的函数需要接收一个指向 map 的指针作为参数,我还需要返回修改后的 map?

    12.2 答案

    1. 如何去表示一个持有指向 Product 指针的变量?
      *Product
    2. 指针类型的零值是多少?
      nil
    3. "解引用(dereferencing)" 是什么意思?
      • 指针是指向存储数据的内存位置的地址。
      • 当我们解引用一个指针时,我们可以访问存储在该地址的内存中的数据。
    4. 如何解引用一个指针?
      使用解引用操作符 *
    5. 填空: ____ 在内部是一个指向 ____ 的指针。
      slice 在内部是一个指向 array 的指针。
    6. 判断正误:当我想函数中修改 map 时,我的函数需要接收一个指向 map 的指针作为参数,我还需要返回修改后的 map
      错, 函数中只要接收一个 map 类型参数就行,也不需要返回更改后的map,因为 map 变量内部存储了指向底层数据的指针

    关键要点

    • 指针是指向数据的地址
    • 类型 *T 表示所有指向 T 类型变量的指针集合
    • 创建指针变量,可以使用运算符&。它将获取一个变量的地址
    userId := 12546584
    p := &userId
    
    `userId` 是 `int` 类型的变量
    `p` 是 `*int` 类型变量
    `*int` 表示所有指向 `int` 类型变量的指针
    
    • 具有指针类型的参数/接收器的函数可以修改指针指向的值。
    • map 和 channel 是“引用类型”
    • 接收 map 或 channel 的函数/方法可以修改内部存储在这两个数据结构中的值(无需传递指向 map 的指针或指向 channel 的指针)
    • 切片在内部保存对数组的引用;任何接收切片的函数/方法都可以修改切片元素。
    • 当你想在函数中修改切片长度和容量时,你应该向该函数传递一个指向切片的指针 (*[]string)
    • 解引用允许你访问和修改存储在指针地址处的值。
    • 要对指针进行解引用操作,请使用运算符 *
    userId := 12546584
    p := &userId
    *p = 4
    log.Println(userId)
    

    p 是一个指针

    • 我们使用 *p 来对指针 p 进行解引用
    • 我们用指令 *p = 4 修改 userId 的值
    • 在代码片段的末尾,userId 的值为 4(不再是 12546584)
    • 当你有一个指向结构的指针时,你可以直接使用你的指针变量访问一个字段(不需要使用解引用运算符)
      • 例子:
    type Cart struct {
        ID string
    }
    var cart Cart
    cartPtr := &cart
    
    • 不需要这样写:(*cartPtr).ID = "1234"
    • 你可直接这样写:cartPtr.Items = "1234"
    • 变量 cart 就会被修改
  • 相关阅读:
    Serialize and Deserialize Binary Tree
    sliding window substring problem汇总贴
    10. Regular Expression Matching
    《深入理解计算机系统》(CSAPP)读书笔记 —— 第七章 链接
    程序员如何写一份合格的简历?(附简历模版)
    9个提高代码运行效率的小技巧你知道几个?
    《深入理解计算机系统》(CSAPP)读书笔记 —— 第六章 存储器层次结构
    24张图7000字详解计算机中的高速缓存
    《深入理解计算机系统》(CSAPP)实验四 —— Attack Lab
    《深入理解计算机系统》(CSAPP)读书笔记 —— 第五章 优化程序性能
  • 原文地址:https://www.cnblogs.com/Zioyi/p/15604530.html
Copyright © 2011-2022 走看看