指针与结构体:地址就是数据.
系列的地基:内存是一排编了号的格子,指针是存着编号的格子;&、*、nil 三个符号,加「Go 只有值拷贝」这一句。
记账单位到手,现在进第一格内存。后面每一篇都会出现 head、tail、cur、prev、tmp——名字换来换去,谜底只有一个:它们都是存着地址的变量。这一篇把这句话拆到不能再拆;拆完之后,链表、树、图的一切增删改,都只是「几个格子换了内容」。
一格内存能存什么.
内存是一排编了号的格子,编号就是地址。一格能存一个值——也能存另一个格子的编号。存编号的格子,就是指针:
x := 100
p := &x // p 的数据 = x 的编号
fmt.Println(p) // 形如 0xc000014098——每次运行都不同图里的地址是示意值——真实地址由运行时分配,每次都变,用 fmt.Println(&x) 可以亲测。不变的是关系:p 的数据栏里躺着 x 的编号。指针没有任何魔法,这一行看懂,全系列的箭头就都看懂了——图上每根箭头,都是某个格子里真实存着的一个编号。
三个符号.
围绕这一格,Go 只给了三个符号:
- &x —— 取编号
- 问一个变量:你住哪格?返回它的地址。任何变量都有地址,因为任何变量都住在某格内存里。
- *p —— 顺着编号走
- 两个身份:写在类型里(*int)表示「存 int 地址的格子」;写在变量前(*p)表示顺着 p 里的编号找到那格,去读或写它——解引用。
- nil —— 还没存编号
- 指针的零值:格子里还没有任何编号。解引用 nil 是运行时崩溃,判空是每个链表操作的第一反应——它也是遍历的停机条件。
var p *int // 能存 int 地址的格子,初值 nil
if p == nil { // 还没指向任何地方
fmt.Println("p is nil")
}
n := 11
p = &n // 存入 n 的编号
*p = 12 // 顺着编号写:n 从此是 12指针能用 == 和 != 比较——比的是编号本身:两个指针相等,意味着它们指着同一格。后面每一篇的遍历都靠它收尾:cur == nil 是走到底。(同一招还能判链表成环——让快慢两个指针跑,slow == fast 就说明有环;本系列用不到,知道原理即可。)
结构体:连续的几格.
一个值一格,那一个「东西」呢?结构体的回答很直白:按声明顺序,占连续的几格:
type User struct {
ID int64
Age int64
}
u := User{ID: 7, Age: 42}
pu := &u
pu.Age = 43 // Go 帮你省了 (*pu).Age 的括号Age 住在基址加 8——字段访问就是「基址 + 偏移」的算术。指向结构体的指针只需要记住基址,剩下的都是加法。系列的记录体记法就从这里来:链表节点 Node{Val, Next} 画成两格方框,因为它在内存里就是两格;下一篇把这套算术放大到 n 个元素,就是数组。
Go 只有值拷贝.
现在回收开篇埋的钥匙句。Go 的赋值和传参只有一种行为:把格子里的数据抄一份。抄的是什么,决定你改得到哪里——经典案例,交换两个数:
func swapByValue(a, b int) {
a, b = b, a // 换的是副本
}
func swapByPointer(a, b *int) {
*a, *b = *b, *a // 顺着地址,换本尊
}传值:换的是副本
传指针:换的是本尊
注意右图的形参拷的也是值——只不过那个值是地址。「传指针」不是第二种传参方式,是同一种:抄数据,数据恰好是编号。所以 cur := head 之后两个变量指着同一个节点,改 cur.Val 会影响 head.Val,但把 cur 挪去下一格不动 head 分毫——前者顺着编号写格子,后者只换了 cur 自己格子里的编号。这一个区分,就是后面所有遍历和增删改的地基。
指针的指针,一瞥.
地址的格子自己也有地址——所以指针可以套娃:
x := 42
p1 := &x // p1 存 x 的编号
p2 := &p1 // p2 存 p1 的编号
**p2 = 100 // 顺两次编号:x 变成 100这个系列用到一层就够。但现在你能读懂任何层:每个 * 就是「顺着编号再走一步」,没有别的含义。
格子、编号、三个符号、一条值拷贝——地基打完。下一篇把「基址 + 偏移」放大成整片连续内存:数组与切片——连续内存的红利。
— 讨论
GitHub评论.
评论由 GitHub Discussions 承载。请围绕文章内容交流,并保持克制、友善。
评论暂时没有加载出来。可以稍后刷新重试,或直接到 GitHub Discussions 参与讨论。