什么是指针
- 指针是指向另一个变量地址的变量。
- Go 语言的指针同时也强调安全性,不会出现迷途指针(dangling pointers)
& 和 * 符号
- 变量会将它们的值存储在计算机的 RAM 里,存储位置就是该变量的内存地址。
- & 表示地址操作符,通过 & 可以获得变量的内存地址。
- & 操作符无法获得字符串/数值/布尔字面值的地址。
- 操作符与 & 的作用相反,它用来解引用,提供内存地址指向的值。
- C 语言中的内存地址可以通过例如 address++ 这样的指针运算进行操作,但是在 Go 里面不允许这种不安全操作。
package main
import "fmt"
func main() {
answer := 42
fmt.Println(&answer)
address := &answer
fmt.Println(*address)
}
指针类型
package main
import "fmt"
func main() {
answer := 42
address := &answer
fmt.Printf("address is a %T\n", address)
}
- 指针类型和其它普通类型一样,出现在所有需要用到类型的地方,如变量声明、函数形参、返回值类型、结构体字段等。
package main
import "fmt"
func main() {
canada := "Canada"
var home *string
fmt.Printf("home is a %T\n", home)
home = &canada
fmt.Println(*home)
}
- 将 * 放在类型前面表示声明指针类型
- 将 * 放在变量前面表示解引用操作
指针就是用来指向的
package main
import "fmt"
func main() {
var administrator *string
scolese := "Christopher J. Scolese"
administrator = &scolese
fmt.Println(*administrator)
bolden := "Charles F. Bolden"
administrator = &bolden
fmt.Println(*administrator)
bolden = "Charles Frank Bolden Jr."
fmt.Println(*administrator)
*administrator = "Maj. Gen. Charles Frank Bolden Jr."
fmt.Println(bolden)
major := administrator
*major = "Major General Charles Frank Bolden Jr."
fmt.Println(bolden)
fmt.Println(administrator == major)
lightfoot := "Robert M. Lightfoot Jr."
administrator = &lightfoot
fmt.Println(administrator == major)
charles := *major
*major = "Charles Bolden"
fmt.Println(charles)
fmt.Println(bolden)
charles = "Charles Bolden"
fmt.Println(charles == bolden)
fmt.Println(&charles == &bolden)
}
- 两个指针变量持有相同的内存地址,那么它们就是相等。
指向结构的指针
- 与字符串和数值不一样,复合字面量的前面可以放置 &。
- 访问字段时,对结构体进行解引用并不是必须的。
package main
import "fmt"
func main() {
type person struct {
name, superpower string
age int
}
timmy := &person{
name: "Timothy",
age: 10,
}
timmy.superpower = "flying"
fmt.Printf("%+v\n", timmy)
}
指向数组的指针
- 和结构体一样,可以把 & 放在数组的复合字面值前面来创建指向数组的指针。
package main
import "fmt"
func main() {
superpowers := &[3]string{"flight", "invisibility", "super strength"}
fmt.Println(superpowers[0])
fmt.Println(superpowers[1:2])
}
- 数组在执行索引或切片操作时会自动解引用。没有必要写 (*superpower)[0] 这种形式。
- 与 C 语言不一样,Go 里面数组和指针式两种完全独立的类型。
- slice 和 map 的复合字面值前面也可以放置 & 操作符,但是 Go 并没有为它们提供自动解引用的功能。
实现修改
- Go 语言的函数和方法都是按值传递参数的,这意味着函数总是操作于被传递参数的副本。
- 当指针被传递到函数时,函数将接收传入的内存地址的副本。之后函数可以通过解引用内存地址来修改指针指向的值。
package main
import "fmt"
type person struct {
name, superpower string
age int
}
func birthday(p *person) {
p.age++
}
func main() {
rebecca := person{
name: "Rebecca",
superpower: "imagination",
age: 14,
}
birthday(&rebecca)
fmt.Printf("%+v\n", rebecca)
}
指针接收者
- 方法的接收者和方法的参数在处理指针方面是很相似的。
package main
import "fmt"
type person struct {
name string
age int
}
func (p *person) birthday() {
p.age++
}
func main() {
terry := &person{
name: "Terry",
age: 15,
}
terry.birthday()
fmt.Printf("%+v\n", terry)
nathan := person{
name: "Nathan",
age: 17,
}
nathan.birthday()
fmt.Printf("%+v\n", nathan)
}
- Go 语言在变量通过点标记法进行调用的时候,自动使用 & 取得变量的内存地址。
- 所以不用写 (&nathan).birthday() 这种形式也可以正常运行。
package main
import (
"fmt"
"time"
)
func main() {
const layout = "Mon, Jan 2, 2006"
day := time.Now()
tomorrow := day.Add(24 * time.Hour)
fmt.Println(day.Format(layout))
fmt.Println(tomorrow.Format(layout))
}
- 使用指针作为接收者的策略应该始终如一:
- 如果一种类型的某些方法需要用到指针作为接收者,就应该为这种类型的所有方法都是用指针作为接收者。
内部指针
- Go 语言提供了 内部指针 这种特性。
- 它用于确定结构体中指定字段的内存地址。
- & 操作符不仅可以获得结构体的内存地址,还可以获得结构体中指定字段的内存地址。
package main
import "fmt"
type stats struct {
level int
endurance, health int
}
func levelUp(s *stats) {
s.level++
s.endurance = 42 + (14 * s.level)
s.health = 5 * s.endurance
}
func main() {
type character struct {
name string
stats stats
}
player := character{name: "Matthias"}
levelUp(&player.stats)
fmt.Printf("%+v\n", player.stats)
}
修改数组
package main
import "fmt"
func reset(board *[8][8]rune) {
board[0][0] = 'r'
// ...
}
func main() {
var board [8][8]rune
reset(&board)
fmt.Printf("%c", board[0][0])
}
隐式的指针
- Go 语言里一些内置的集合类型就在暗中使用指针。
- map 在被赋值或者被作为参数传递的时候不会被复制。
- map 就是一种隐式指针。
- 这种写法就是多此一举:func demolish(planets *map[string]string)
- map 的键和值都可以是指针类型
- 需要将指针指向 map 的情况并不多见
slice 指向数组
- 之前说过 slice 是指向数组的窗口,实际上 slice 在指向数组元素的时候也使用了指针。
- 每个 slice 内部都会被表示为一个包含 3 个元素的结构,它们分别指向:
- 当 slice 被直接传递至函数或方法时,slice 的内部指针就可以对底层数据进行修改。
- 指向 slice 的显式指针的唯一作用就是修改 slice 本身:slice 的长度、容量以及起始偏移量。
package main
import "fmt"
func reclassify(planets *[]string) {
*planets = (*planets)[0:8]
}
func main() {
planets := []string{
"Mercury", "Venus", "Earth", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune",
"Pluto",
}
reclassify(&planets)
fmt.Println(planets)
}
指针和接口
package main
import (
"fmt"
"strings"
)
type talker interface {
talk() string
}
func shout(t talker) {
louder := strings.ToUpper(t.talk())
fmt.Println(louder)
}
type martian struct{}
func (m martian) talk() string {
return "nack nack"
}
func main() {
shout(martian{})
shout(&martian{})
}
- 本例中,无论 martian 还是指向 martian 的指针,都可以满足 talker 接口。
- 如果方法使用的是指针接收者,那么情况会有所不同。
package main
import (
"fmt"
"strings"
)
type talker interface {
talk() string
}
func shout(t talker) {
louder := strings.ToUpper(t.talk())
fmt.Println(louder)
}
type laser int
func (l *laser) talk() string {
return strings.Repeat("pew ", int(*l))
}
func main() {
pew := laser(2)
shout(&pew)
}
明智的使用指针
作业题
- 编写一个可以让海龟上下左右移动的程序:
- 程序中的海龟需要存储一个位置(x,y)
- 正数坐标表示向下或向右
- 通过使用方法对相应的变量实施自增和自减来实现移动
- 请使用 main 函数测试这些方法并打印出海龟的最终位置
package main
import "fmt"
type turtle struct {
x, y int
}
func (t *turtle) up() {
t.y--
}
func (t *turtle) down() {
t.y++
}
func (t *turtle) left() {
t.x--
}
func (t *turtle) right() {
t.x++
}
func main() {
var t turtle
t.up()
t.up()
t.left()
t.left()
fmt.Println(t)
t.down()
t.down()
t.right()
t.right()
fmt.Println(t)
}