|
|
@@ -0,0 +1,183 @@
|
|
|
+# 第 09 课:指针
|
|
|
+
|
|
|
+## 学习目标
|
|
|
+
|
|
|
+学会区分变量的值、变量的地址和指针变量,理解怎样通过函数修改调用方的数据,以及测试参数 `t *testing.T` 中星号的含义。
|
|
|
+
|
|
|
+按 [main.go](./main.go) 的 1~7 节逐段手敲。本课只使用普通指针和结构体,不涉及二级指针、unsafe 或复杂内存管理。共享根目录的 `go.mod`,无需第三方依赖。
|
|
|
+
|
|
|
+## 1. 三个写法,分别看待
|
|
|
+
|
|
|
+```go
|
|
|
+number := 10
|
|
|
+var pointer *int = &number
|
|
|
+fmt.Println(*pointer)
|
|
|
+```
|
|
|
+
|
|
|
+| 写法 | 含义 |
|
|
|
+| --- | --- |
|
|
|
+| `number` | 一个整数变量,当前值为 10 |
|
|
|
+| `&number` | 取得 number 的地址 |
|
|
|
+| `*int` | 类型:指向 int 的指针 |
|
|
|
+| `pointer` | 保存地址的指针变量 |
|
|
|
+| `*pointer` | 表达式:访问 pointer 指向的变量,称为解引用 |
|
|
|
+
|
|
|
+星号的位置不同,含义不同:声明中的 `*int` 是类型,表达式中的 `*pointer` 是访问目标。`*pointer = 20` 修改目标整数;`pointer = &other` 则让指针改为指向另一个整数。
|
|
|
+
|
|
|
+可以用 `fmt.Printf("地址:%p\n", pointer)` 观察地址。具体地址不固定,不要背诵某次输出,也不要依赖两次运行地址相同。示例用地址比较保证默认输出稳定。
|
|
|
+
|
|
|
+## 2. 函数参数仍然按值传递
|
|
|
+
|
|
|
+普通参数:
|
|
|
+
|
|
|
+```go
|
|
|
+func changeValue(value int) {
|
|
|
+ value = 100
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+调用 `changeValue(number)` 只修改整数副本,不影响 number。
|
|
|
+
|
|
|
+指针参数:
|
|
|
+
|
|
|
+```go
|
|
|
+func changeByPointer(value *int) bool {
|
|
|
+ if value == nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ *value = 100
|
|
|
+ return true
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+调用 `changeByPointer(&number)` 时,把地址值复制给参数 value。value 和调用方都能找到同一个整数,因此通过 `*value` 可以修改 number。
|
|
|
+
|
|
|
+**Go 仍然是按值传递,不是出现指针后就变成另一种参数传递规则。** 复制的是地址值,而不是目标整数。
|
|
|
+
|
|
|
+另外,在函数内写 `value = &local` 只改变参数指针的副本,不会改变调用方指针本身。示例中的 redirect 专门演示这个区别。本课不需要掌握怎样修改调用方指针的指向。
|
|
|
+
|
|
|
+## 3. nil 指针
|
|
|
+
|
|
|
+```go
|
|
|
+var empty *int
|
|
|
+```
|
|
|
+
|
|
|
+指针零值是 nil,表示没有指向有效变量。可以判断 `empty == nil`,但不能执行 `*empty`,否则会发生运行时 panic。
|
|
|
+
|
|
|
+指针参数是否允许 nil,取决于函数约定。本课函数主动检查,遇到 nil 返回 false;不能因此假设所有库函数都接受 nil。
|
|
|
+
|
|
|
+`var empty *int` 不会自动创建一个目标整数。与之不同:
|
|
|
+
|
|
|
+```go
|
|
|
+created := new(int)
|
|
|
+```
|
|
|
+
|
|
|
+会创建一个零值 int 并返回它的指针,`created != nil`,`*created == 0`。通常 `number := 0; pointer := &number` 也能满足类似需求,不必强制使用 new。
|
|
|
+
|
|
|
+## 4. 结构体指针
|
|
|
+
|
|
|
+```go
|
|
|
+func birthday(student *Student) bool {
|
|
|
+ if student == nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ student.Age++
|
|
|
+ return true
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+调用 `birthday(&student)`,修改的是原学生的年龄。
|
|
|
+
|
|
|
+Go 允许直接写 `student.Age`,这里等价于 `(*student).Age`。Go 没有 C 中的 `->` 运算符;nil 结构体指针也不能直接访问字段。
|
|
|
+
|
|
|
+## 5. 再看测试参数
|
|
|
+
|
|
|
+```go
|
|
|
+func TestAdd(t *testing.T) {
|
|
|
+ // t.Fatalf(...) 等操作由 testing 包提供。
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+现在可以拆开理解:
|
|
|
+
|
|
|
+- t:参数变量名。
|
|
|
+- testing.T:testing 包中提供的测试管理类型。
|
|
|
+- `*testing.T`:指向这个类型的指针。
|
|
|
+- 测试框架调用函数时传入 t,用户不需要自己创建或调用 TestAdd。
|
|
|
+
|
|
|
+`t.Fatalf` 不是结构体里的普通数据字段,而是 testing.T 提供的方法,下一课学习“方法”时再深入理解。本课不需要手动创建 testing.T。
|
|
|
+
|
|
|
+## 6. 与 C 相比
|
|
|
+
|
|
|
+- 都用 `&` 取地址,用 `*` 声明指针类型或解引用。
|
|
|
+- Go 的指针不支持普通 C 风格的 `p++`、`p + 1` 指针算术。
|
|
|
+- 普通 Go 代码不需要手动 malloc / free,内存由运行时垃圾回收管理;文件等系统资源仍需要适当关闭。
|
|
|
+- 可以返回局部变量的地址,Go 会保证仍被使用的数据保持有效;不要直接套用 C 的栈上悬空指针规则。具体分配在栈还是堆由编译器分析,不由 `&` 或 new 单独决定。
|
|
|
+- 指针能修改共享变量,但不自动提供并发安全,后续并发课程再讨论同步。
|
|
|
+
|
|
|
+## 编译并运行
|
|
|
+
|
|
|
+在**仓库根目录**执行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+New-Item -ItemType Directory -Force bin/09_pointers | Out-Null
|
|
|
+go build -o ./bin/09_pointers/pointers.exe ./lessons/09_pointers
|
|
|
+.\bin\09_pointers\pointers.exe
|
|
|
+```
|
|
|
+
|
|
|
+修改源码后需要重新编译。快速编译并运行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+go run ./lessons/09_pointers
|
|
|
+```
|
|
|
+
|
|
|
+或者从根目录进入本课:
|
|
|
+
|
|
|
+```powershell
|
|
|
+Set-Location lessons/09_pointers
|
|
|
+go run .
|
|
|
+```
|
|
|
+
|
|
|
+## 预期输出
|
|
|
+
|
|
|
+```text
|
|
|
+=== 取地址与解引用 ===
|
|
|
+number 的值:10
|
|
|
+指针类型:*int,是否指向 number:true
|
|
|
+通过 *pointer 读取:10
|
|
|
+通过指针修改后,number:20
|
|
|
+
|
|
|
+=== 函数修改原变量 ===
|
|
|
+普通参数调用后:20
|
|
|
+指针参数调用后:100,成功:true
|
|
|
+
|
|
|
+=== 指针复制与重新指向 ===
|
|
|
+通过 alias 修改后,number:30
|
|
|
+函数内重新指向后,原指针仍指向 number:true
|
|
|
+
|
|
|
+=== nil 指针 ===
|
|
|
+空指针为 nil:true
|
|
|
+传入 nil,修改成功:false
|
|
|
+
|
|
|
+=== 结构体指针 ===
|
|
|
+小朱 过生日后,年龄:21
|
|
|
+
|
|
|
+=== new 创建指针 ===
|
|
|
+指针为 nil:false,初始值:0
|
|
|
+修改后的值:42
|
|
|
+```
|
|
|
+
|
|
|
+## 动手练习
|
|
|
+
|
|
|
+- [ ] 用 `%p` 打印 `&number` 和 pointer,确认它们相等。
|
|
|
+- [ ] 创建另一个整数 other,让 pointer 指向它,观察 `*pointer = 50` 修改了谁。
|
|
|
+- [ ] 给 birthday 传入 nil,检查返回结果。
|
|
|
+- [ ] 新写 `reset(value *int) bool`,检查 nil 后把目标整数设为 0。
|
|
|
+- [ ] 对比 `var p *int` 与 `p := new(int)` 的 nil 状态和是否能解引用。
|
|
|
+- [ ] 回看第七课的 `t *testing.T`,区分参数名、包名和类型。
|
|
|
+
|
|
|
+如果试验 nil 解引用,一次只保留这一处故意错误,观察 panic 后恢复注释。若另行手敲,放到本课的 `practice/` 子目录,用 `go run ./lessons/09_pointers/practice` 运行,不要在同一个包中重复定义 Student 或 main。
|
|
|
+
|
|
|
+下一课学习方法:理解接收者、值接收者与指针接收者,让结构体拥有自己的操作。
|
|
|
+
|
|
|
+[上一课:结构体](../08_structs/README.md) · [学习总览](../../README.md)
|