Kodewick
开始学 →
第 11 课,共 24 课目录

指针

第 11 课,共 24 课 · 第 3 部分 · 类型 · 约 15 分钟

每个变量都放在内存里的某个位置,这个位置叫地址。指针是存着一个地址的值:它指向一个变量。通过指针,可以读、也可以改它指向的那个变量。

prog.go
1package main2 3import "fmt"4 5func main() {6	price := 1007	p := &price8	fmt.Println(*p)9	*p = 8010	fmt.Println(price)11}
输出
100
80
写法意思
&priceprice 的地址。
p := &pricep 指向 price。
*pp 指向的那个变量:可以读,也可以赋值。

*p = 80 改的就是 price。点单步,看 p 画成一个箭头,指着 price。地址本身是一串数字,每次运行可能不一样,所以这门课不显示它。

指针的类型

指向 int 的指针,类型是 *int;指向 string 的是 *string。指针的零值是 nil:什么也不指。

prog.go
1package main2 3import "fmt"4 5func main() {6	count := 37	var p *int8	fmt.Println(p == nil)9	p = &count10	fmt.Printf("%T %d\n", p, *p)11	q := new(int)12	*q = 713	fmt.Println(*q)14}
输出
true
*int 3
7

new(int) 新建一个值为零的 int,返回指向它的指针。它不常用:多数时候用 & 就够了。

函数拿到的是复制

第 5 课讲过:调用函数时,参数是一份复制。函数里改它,外面的变量不变:

prog.go
1package main2 3import "fmt"4 5func addBonus(salary int) {6	salary += 5007	fmt.Println("函数里", salary)8}9 10func main() {11	pay := 300012	addBonus(pay)13	fmt.Println("函数外", pay)14}
输出
函数里 3500
函数外 3000

传指针

想让函数改调用者的变量,就把变量的地址传进去。函数通过指针改,改的就是外面那个:

prog.go
1package main2 3import "fmt"4 5func addBonus(salary *int) {6	*salary += 5007}8 9func main() {10	pay := 300011	addBonus(&pay)12	fmt.Println(pay)13}
输出
3500

点单步,走进 addBonus:salary 是一个箭头,指着 main 里的 pay。和上一段比一比,那里的 salary 是一个装着 3000 的新格子。

先猜一猜

这段程序显示什么?

1package main2 3import "fmt"4 5func double(n *int) {6	*n = *n * 27}8 9func main() {10	a := 511	b := a12	double(&b)13	fmt.Println(a, b)14}

结构体指针

指针最常指向结构体。通过结构体指针取字段,直接写 a.Balance 就行,Go 会自动帮你写成 (*a).Balance:

prog.go
1package main2 3import "fmt"4 5type Account struct {6	Owner   string7	Balance int8}9 10func deposit(a *Account, amount int) {11	a.Balance += amount12}13 14func main() {15	acc := Account{Owner: "小林", Balance: 100}16	deposit(&acc, 50)17	fmt.Println(acc.Balance)18	p := &Account{Owner: "小王"}19	deposit(p, 30)20	fmt.Println(p.Owner, p.Balance)21	fmt.Println(p)22}
输出
150
小王 30
&{小王 30}

&Account{…} 新建一个结构体,同时拿到它的指针。显示结构体指针时,fmt 在前面加一个 &。

nil 指针

一个 nil 指针什么也不指。通过它读或写,程序会 panic:

prog.go
1package main2 3import "fmt"4 5type Account struct {6	Owner   string7	Balance int8}9 10func main() {11	var acc *Account12	fmt.Println(acc == nil)13	acc.Balance = 10014	fmt.Println("到不了这里")15}
输出
true
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x?]

goroutine 1 [running]:
main.main(...)
	/tmp/sandbox/prog.go:13 +0x1d
exit status 2

发生了什么: 用了一个 nil 指针(它没有指向任何东西),比如没初始化的 *T 去取字段或调用方法。

invalid memory address or nil pointer dereference 的意思是:想通过一个 nil 指针去找变量,可它没有指向任何东西。点「看看是怎么出错的」,停在第 13 行。不确定一个指针是不是 nil 时,先用 if acc != nil 检查。

什么时候用指针

  • 函数要改调用者的变量或结构体时。
  • 结构体很大,不想每次传参都复制一份时。
  • 想用 nil 表示「还没有」时。

切片和 map 不用传指针也能改到里面的元素:它们本来就和调用者共用底层的数据(第 7、8 课)。

prog.go
1package main2 3import "fmt"4 5func discount(prices []int) {6	for i := range prices {7		prices[i] = prices[i] * 9 / 108	}9}10 11func main() {12	prices := []int{100, 50}13	discount(prices)14	fmt.Println(prices)15}
输出
[90 45]

不过,函数里 append 让切片变长,外面是看不到的:那种情况让函数把新切片返回。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

用指针交换两个数

写好 swap,让它交换两个指针指向的变量的值。程序交换了两组数。

要显示
2 1
70 30
exercise1.go
1package main2 3import "fmt"4 5func swap(a, b *int) {6}7 8func main() {9	x, y := 1, 210	swap(&x, &y)11	fmt.Println(x, y)12	m, n := 30, 7013	swap(&m, &n)14	fmt.Println(m, n)15}
输出
点「运行」或按 Ctrl+Enter
练习 2

给商品打折

写好 applyDiscount,通过指针把商品的价格降低 percent%(percent 是 10 就是打九折)。程序给两件商品打了折。

要显示
台灯 180
水杯 60
exercise2.go
1package main2 3import "fmt"4 5type Product struct {6	Name  string7	Price int8}9 10func applyDiscount(p *Product, percent int) {11}12 13func main() {14	lamp := Product{Name: "台灯", Price: 200}15	mug := Product{Name: "水杯", Price: 75}16	applyDiscount(&lamp, 10)17	applyDiscount(&mug, 20)18	fmt.Println(lamp.Name, lamp.Price)19	fmt.Println(mug.Name, mug.Price)20}
输出
点「运行」或按 Ctrl+Enter
练习 3

修好它:没有账户的指针

这段程序会 panic。让 acc 指向一个属于「小林」、余额为 0 的账户,程序就会显示存了两次钱以后的余额。

要显示
小林 100
小林 150
exercise3.go
1package main2 3import "fmt"4 5type Account struct {6	Owner   string7	Balance int8}9 10func deposit(a *Account, amount int) {11	a.Balance += amount12}13 14func main() {15	var acc *Account16	deposit(acc, 100)17	fmt.Println(acc.Owner, acc.Balance)18	deposit(acc, 50)19	fmt.Println(acc.Owner, acc.Balance)20}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • &x 是 x 的地址;p := &x 让 p 指向 x,p 的类型是 *int。
  • *p 是 p 指向的变量,可以读也可以改;指针的零值是 nil。
  • 函数参数是复制;要改调用者的变量,传它的地址 f(&x),函数里用 *p 改。
  • 结构体指针取字段直接写 p.Balance;&Account{…} 新建结构体并拿到指针。
  • 通过 nil 指针读写会 panic:nil pointer dereference。
  • 切片和 map 不用指针也能改到元素。
这一课学完了吗?点一下标记。进度存在这个浏览器里,随时可以撤销。