15

关于golang指针的理解与使用

 3 years ago
source link: https://studygolang.com/articles/29273
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Go指针理解

Go 有指针,但是没有指针运算。你不能用指针变量遍历字符串的各个字节。在 Go 中调用函数的时候,得记得变量是值传递的。

通过类型作为前缀来定义一个指针’ * ’:var p * int。现在 p 是一个指向整数值的指针。所有新定义的变量都被赋值为其类型的零值,而指针也一样。一个新定义的或者没有任何指向的指针,有值 nil。在其他语言中,这经常被叫做空(NULL)指针,在 Go 中就是 nil 。让指针指向某些内容,可以使用取址操作符 ( & ),像这样:

package main 
 
import "fmt"
 
func main() {
    var p *int 
    fmt.Printf("%v\n",p) //← 打印 nil
 
    var i int //← 定义一个整形变量 i
    p = &i    //← 使得 p 指向 i, 获取 i 的地址
    fmt.Printf("%v\n",p) //打印内存地址
 
    *p = 6
    fmt.Printf("%v\n",*p) //打印6
    fmt.Printf("%v\n",i) //打印6
 
}

前面已经说了,没有指针运算,所以如果这样写: p++ ,它表示 ( p)++ :首先获取指针指向的值,然后对这个值加一。这里注意与C语言的区别。

对于Go语言,严格意义上来讲,只有一种传递,也就是按值传递(by value)。当一个变量当作参数传递的时候,会创建一个变量的副本,然后传递给函数或者方法,你可以看到这个副本的地址和变量的地址是不一样的。

当变量当做指针被传递的时候,一个新的指针被创建,它指向变量指向的同样的内存地址,所以你可以将这个指针看成原始变量指针的副本。当这样理解的时候,我们就可以理解成Go总是创建一个副本按值转递,只不过这个副本有时候是变量的副本,有时候是变量指针的副本。

指针与普通变量区别

刚学习golang的人可能不是很明白普通变量和指针的区别,看到有人说其实普通变量是程序创造出来的,比如说 c 中

a int

那么在编译时 就会有 [a 地址 int] 这样来标识内存

普通变量a其实是语言本身创造了,是为了更方便的表示内存。我们对a进行访问其实就是直接对内存进行访问。至于a表示的内存的地址是多少,程序员一般不用关心。编译器会自动分配地址,也就是常说的为a分配一个地址。如果想知道a的地址也可以通过&a得知。

打个比方来理解:普通变量就像是房间(内存)外面的门牌号(总经理室),指针就是这个房间的地址(9#905)

变量是运行时系统给这个内存起的别名,内存地址是唯一的,程序中当我想拿到这个内存的值的时候,因为知道它的别名,所以直接用别名访问就可以得到值,又或者我能知道它唯一的地址我也能得到它的值,其实是两种不同的内存访问方式,但是变量是会变的,地址是不会变的。比如在go中

package main
 
func main(){
    a := 10   //此时有一块内存存放了10,它的地址由系统自动分配,别名是a
    a = 20   //内存存放的10变成了20
    var p *int
    p = &a   //或者直接写 p := &a
    //上面的p是一个指针,通过 *p 的方式同样可以访问 变量a指向 的内存
 
    /*当你动态申请内存的时候,指针的存在意义之一就被体现出来了*/ 
    ptr := new(int)   
    //申请了一块内存空间,没有办法指定别名,new()返回内存地址,用指针接收
    //此时并没有变量能直接指向这块内存,所以只能通过内存地址来访问
}

看到有个解释还可以,是说指针和普通变量区别的

1+2这个表达式永远得值3

a+b这个表达式只依赖于a,b的值—–按名字访问称为直接访问

*p + *q 这个表达式的值随着p,q指向的变量不同而不同—-按指针访问称为间接访问

指针的使用

方法中的指针

方法即为有接受者的函数,接受者可以是类型的实例变量或者是类型的实例指针变量。但两种效果不同。

1、类型的实例变量

func main(){
    person := Person{"TigerwolfC", 25}
    fmt.Printf("person<%s:%d>\n", person.name, person.age)
    person.sayHi()
    person.ModifyAge(28)
    person.sayHi()
}
type Person struct {
    name string
    age int
}
func (p Person) sayHi() {
    fmt.Printf("SayHi -- This is %s, my age is %d\n",p.name, p.age)
}
func (p Person) ModifyAge(age int) {
    fmt.Printf("ModifyAge")
    p.age = age
}

输出结果

person<TigerwolfC:25>
SayHi -- This is TigerwolfC, my age is 25
ModifyAgeSayHi -- This is TigerwolfC, my age is 25

尽管 ModifyAge 方法修改了其age字段,可是方法里的p是person变量的一个副本,修改的只是副本的值。下一次调用sayHi方法的时候,还是person的副本,因此修改方法并不会生效。即实例变量的方式并不会改变接受者本身的值。

2、类型的实例指针变量

package main
 
import "fmt"
 
func main(){
    person := Person{"TigerwolfC", 25}
    fmt.Printf("person<%s:%d>\n", person.name, person.age)
    person.sayHi()
    person.ModifyAge(28)
    person.sayHi()
}
 
type Person struct {
    name string
    age int
}
 
func (p *Person) sayHi() {
    fmt.Printf("SayHi -- This is %s, my age is %d\n",p.name, p.age)
}
 
func (p *Person) ModifyAge(age int) {
    fmt.Printf("ModifyAge")
    p.age = age
}

输出结果

person<TigerwolfC:25>
SayHi -- This is TigerwolfC, my age is 25
ModifyAgeSayHi -- This is TigerwolfC, my age is 28

Go会根据Person的示例类型,转换成指针类型再拷贝,即 person.ModifyAge会变成 (&person).ModifyAge。

指针类型的接受者,如果实例对象是值,那么go会转换成指针,然后再拷贝,如果本身就是指针对象,那么就直接拷贝指针实例。因为指针都指向一处值,就能修改对象了。

3.如何选择 P 和 *P

在定义函数和方法的时候,需要对函数的参数和返回值定义成P和

P深思熟虑,有些情况下可能还会有些困惑。

那么什么时候才应该把参数定义成类型P,什么情况下定义成类型

P呢。

一般的判断标准是看副本创建的成本和需求。

不想变量被修改。 如果你不想变量被函数和方法所修改,那么选择类型P。相反,如果想修改原始的变量,则选择

P

P,只创建新的指针,这个区别是巨大的

(不针对函数参数,只针对本地变量/本地变量)对于函数作用域内的参数,如果定义成P,Go编译器尽量将对象分配到栈上,而*P很可能会分配到对象上,这对垃圾回收会有影响

4. 零值与nil(空指针)

变量声明而没有赋值,默认为零值,不同类型零值不同,例如字符串零值为空字符串;

指针声明而没有赋值,默认为nil,即该指针没有任何指向。当指针没有指向的时候,不能对(*point)进行操作包括读取,否则会报空指针异常。

func main(){
    // 声明一个指针变量 aPot 其类型也是 string
    var aPot *string
    fmt.Printf("aPot: %p %#v\n", &aPot, aPot) // 输出 aPot: 0xc42000c030 (*string)(nil)
    *aPot = "This is a Pointer"  // 报错: panic: runtime error: invalid memory address or nil pointer dereference
}

解决方法即给该指针分配一个指向,即初始化一个内存,并把该内存地址赋予指针变量,例如:// 声明一个指针变量 aPot 其类型也是 string

var aPot *string
  fmt.Printf("aPot: %p %#v\n", &aPot, aPot) // 输出 aPot: 0xc42000c030 (*string)(nil)
  aPot = &aVar
  *aPot = "This is a Pointer"
  fmt.Printf("aVar: %p %#v \n", &aVar, aVar) // 输出 aVar: 0xc42000e240 "This is a Pointer"
  fmt.Printf("aPot: %p %#v %#v \n", &aPot, aPot, *aPot) // 输出 aPot: 0xc42000c030 (*string)(0xc42000e240) "This is a Pointer"

或者通过new开辟一个内存,并返回这个内存的地址。

var aNewPot *int
aNewPot = new(int)
*aNewPot = 666
fmt.Printf("aNewPot: %p %#v %#v \n", &aNewPot, aNewPot, *aNewPot) // 输出 aNewPot: 0xc42007a028 (*int)(0xc42006e1f0) 666

总结

Golang提供了指针用于操作数据内存,并通过引用来修改变量。

只声明未赋值的变量,golang都会自动为其初始化为零值,基础数据类型的零值比较简单,引用类型和指针的零值都为nil,nil类型不能直接赋值,因此需要通过new开辟一个内存,或者通过make初始化数据类型,或者两者配合,然后才能赋值。

指针也是一种类型,不同于一般类型,指针的值是地址,这个地址指向其他的内存,通过指针可以读取其所指向的地址所存储的值。

函数方法的接受者,也可以是指针变量。无论普通接受者还是指针接受者都会被拷贝传入方法中,不同在于拷贝的指针,其指向的地方都一样,只是其自身的地址不一样。

欢迎关注我们的微信公众号,每天学习Go知识

FveQFjN.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK