关于go:借问变量何处存牧童笑称用指针Go-lang118入门精炼教程由白丁入鸿儒go-lang指针的使用EP05

45次阅读

共计 4279 个字符,预计需要花费 11 分钟才能阅读完成。

指针是指什么?指针是存储另一个变量的内存地址的变量。变量是一种使用方便的占位符,用于援用计算机内存地址,一个指针变量能够指向任何一个值的内存地址它指向那个值的内存地址。类比的话,指针就是书籍中的目录,自身也占据书页,既能够通过目录取得章节内容,又能够指向具体章节的页数(地址)。

指针申明

申明指针,* T 是指针变量的类型,它指向 T 类型的值:

var var_name *var-type

var-type 为指针类型,var\_name 为指针变量名,* 号用于指定变量是作为一个指针。

例如:

var ip *int        /* 指向整型 */  
var fp *float32    /* 指向浮点型 */

之前咱们已经应用 & 关键字来获取变量在内存中的地址,事实上,该对象就是指针:

package main  
  
import "fmt"  
  
func main() {  
    var a int = 20 /* 申明理论变量 */  
    var ip *int    /* 申明指针变量 */  
  
    ip = &a /* 指针变量的存储地址 */  
  
    fmt.Printf("a 变量的地址是: %x\n", &a)  
  
    /* 指针变量的存储地址 */  
    fmt.Printf("ip 变量的存储地址: %x\n", ip)  
  
    /* 应用指针拜访值 */  
    fmt.Printf("*ip 变量的值: %d\n", *ip)  
}

由此可见,指针变量的类型为 *Type,该指针指向一个 Type 类型的变量。指针变量最大的特点就是存储的某个理论变量的内存地址,通过记录某个变量的地址,从而间接的操作该变量。

& 关键字能够从一个变量中取到其内存地址。

* 关键字如果在赋值操作值的右边,指该指针指向的变量;* 关键字如果在赋值操作符的左边,指从一个指针变量中获得变量值,又称指针的解援用。

指针也分不同的类型:

package main  
  
import "fmt"  
  
func main() {  
    mystr := "字符串"  
    myint := 1  
    mybool := false  
    myfloat := 3.2  
    fmt.Printf("type of &mystr is :%T\n", &mystr)  
    fmt.Printf("type of &myint is :%T\n", &myint)  
    fmt.Printf("type of &mybool is :%T\n", &mybool)  
    fmt.Printf("type of &myfloat is :%T\n", &myfloat)  
}

程序返回:

type of &mystr is :*string  
type of &myint is :*int  
type of &mybool is :*bool  
type of &myfloat is :*float64

但其实,指针的类型,其实就是它所指的变量的根本类型,二者类型是统一的。

空指针

Go lang 空指针是当一个指针被定义后没有调配到任何变量时,它的值为 nil。nil 指针也称为空指针。nil 在概念上和其它语言的 null、None、nil、NULL 一样,都指代零值或空值。一个指针变量通常缩写为 ptr:

if(ptr != nil)     /* ptr 不是空指针 */  
if(ptr == nil)    /* ptr 是空指针 */

具体例子:

package main  
  
import "fmt"  
  
func main() {  
    x := "字符串"  
    var ptr *string  
    fmt.Println("ptr is", ptr)  
    ptr = &x  
    fmt.Println("ptr is", ptr)  
}

程序返回:

ptr is  <nil>  
ptr is  0xc00003c250

但也须要留神的是,指针的空值和变量的空值一样,都须要用恒等或者非恒等来判断,而并非像 Python 一样应用 is 关键字去比对内存的具体地址。

指针操作

获取一个指针意味着拜访指针指向的变量的值。语法是:*a

package main  
  
import ("fmt")  
  
func main() {  
    b := 255  
    a := &b  
    fmt.Println("address of b is", a)  
    fmt.Println("value of b is", *a)  
}

程序返回:

address of b is 0xc000014088  
value of b is 255

个别状况下,咱们能够通过二次赋值来扭转变量的值,当初通过指针也能够:

package main  
  
import ("fmt")  
  
func main() {  
    b := 255  
    a := &b  
    fmt.Println("address of b is", a)  
    fmt.Println("value of b is", *a)  
    *a++  
    fmt.Println("new value of b is", b)  
}

程序返回:

address of b is 0xc0000aa058  
value of b is 255  
new value of b is 256

这里值须要通过对指针进行递增操作,就能够批改变量 b 的值。

与此同时,在传参过程中,也能够应用指针:

package main  
  
import ("fmt")  
  
func change(val *int) {*val = 55}  
func main() {  
    a := 58  
    fmt.Println("value of a before function call is", a)  
    b := &a  
    change(b)  
    fmt.Println("value of a after function call is", a)  
}

程序返回:

value of a before function call is 58  
value of a after function call is 55

家喻户晓,int 是值类型数据,如果通过变量进行传递到办法作用域中,办法内作用域内操作的实际上是另外一个对象,比方:

package main  
  
import ("fmt")  
  
func change(val int) {val = 55}  
func main() {  
  
    b := 58  
    change(b)  
    fmt.Println("value of a after function call is", b)  
}

返回:

value of a after function call is 58

但如果传参过程中应用指针,将 a 变量的指针对象传递到办法内,办法内批改的其实是内存地址变量,如此就能够将值类型对象的值对应更改,节俭了额定的内存申请空间。

假如咱们想对办法内的数组进行一些批改,并且对调用者能够看到办法内的数组所做的更改。一种办法是将一个指向数组的指针传递给办法:

package main  
  
import ("fmt")  
  
func modify(arr *[3]int) {(*arr)[0] = 90  
}  
  
func main() {a := [3]int{89, 90, 91}  
    modify(&a)  
    fmt.Println(a)  
}

程序返回:

[90 90 91]

尽管能够用指针传递给一个数组作为办法的实参并对其进行批改,但这并不意味着咱们肯定得这么干,因为还能够应用切片:

package main  
  
import ("fmt")  
  
func modify(sls []int) {sls[0] = 90  
}  
  
func main() {a := [3]int{89, 90, 91}  
    modify(a[:])  
    fmt.Println(a)  
}

程序返回:

[90 90 91]

因为切片与指针一样是援用类型,如果咱们想通过一个函数扭转一个数组的值,能够将该数组的切片当作参数传给函数,也能够将这个数组的指针当作参数传给函数,不言而喻,应用切片更加不便。

此外,指针也能够领有指针,也就是说,指针也能够指向别的指针所在的内存地址:

package main  
  
import "fmt"  
  
func main() {  
  
    var a int  
    var ptr *int  
    var pptr **int  
  
    a = 3000  
  
    /* 指针 ptr 地址 */  
    ptr = &a  
  
    /* 指向指针 ptr 地址 */  
    pptr = &ptr  
  
    /* 获取 pptr 的值 */  
    fmt.Printf("变量 a = %d\n", a)  
    fmt.Printf("指针变量 *ptr = %d\n", *ptr)  
    fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)  
}

程序返回:

 变量 a = 3000  
指针变量 *ptr = 3000  
指向指针的指针变量 **pptr = 3000

这里指针 pptr 指向指针 ptr,指针 ptr 执行变量 a

当咱们扭转指针的指针值:

package main  
  
import "fmt"  
  
func main() {  
  
    var a int  
    var ptr *int  
    var pptr **int  
  
    a = 3000  
  
    /* 指针 ptr 地址 */  
    ptr = &a  
  
    /* 指向指针 ptr 地址 */  
    pptr = &ptr  
  
    /* 获取 pptr 的值 */  
    fmt.Printf("变量 a = %d\n", a)  
    fmt.Printf("指针变量 *ptr = %d\n", *ptr)  
    fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)  
  
    **pptr = 200  
  
    fmt.Printf("变量 a = %d\n", a)  
    fmt.Printf("指针变量 *ptr = %d\n", *ptr)  
    fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)  
}

程序返回:

 变量 a = 3000  
指针变量 *ptr = 3000  
指向指针的指针变量 **pptr = 3000  
变量 a = 200  
指针变量 *ptr = 200  
指向指针的指针变量 **pptr = 200

能够看到产生了连锁反应,起始指向和最终指向都产生了变动,堪称是牵一发而动全身,事实上,指针操作要比反复赋值更加快捷。

结语

简而言之,很多编译型语言都在事实上存在指针,c/c++ 是实在的指针,而 Java 中其实是指针的援用,能够了解为不能操作指针的值,不容许指针运算的指针。事实问题是,go lang 这种“次时代”的新潮流编程语言,为什么不像 Java 那样,仅仅实现“援用”,而肯定非得给出“指针”的本质概念呢?

在 Go lang 官网文档中,能够窥得些许端倪:

In a function call, the function value and arguments are evaluated in the usual order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution.
文档地址:https://go.dev/ref/spec#Calls

一望而知,go lang 的设计者们在 go lang 语法设计上存在“完美主义强迫症”,办法传参是相对的传值,Go lang 中办法传参只有值传递一种形式,不存在援用传递,这样一来,必须有明确的指针类型,才能够保障在传值的前提下能对对象进行批改。

其实 Python 也在此处做出了斗争,可变数据类型进行援用传递,但 go lang 作为钢铁直男,宁愿减少更简单的指针逻辑,也要彻底贯彻值传递逻辑,为的就是在适当的中央应用指针, 对程序运行速度和内存耗费有所增益。

正文完
 0