Преглед изворни кода

feat: 新增第九课指针例程与参数传递说明

zhuts пре 3 дана
родитељ
комит
888ec3e737
3 измењених фајлова са 277 додато и 3 уклоњено
  1. 7 3
      README.md
  2. 183 0
      lessons/09_pointers/README.md
  3. 87 0
      lessons/09_pointers/main.go

+ 7 - 3
README.md

@@ -34,9 +34,12 @@
     │   ├── README.md       # 参数、返回值、值传递和练习
     │   ├── main.go         # 普通函数、多返回值、可变参数
     │   └── main_test.go    # 本课函数测试
-    └── 08_structs/         # 第 08 课:结构体
-        ├── README.md       # 定义、字段、复制和结构体切片
-        └── main.go         # 学生记录与简化测试数据表
+    ├── 08_structs/         # 第 08 课:结构体
+    │   ├── README.md       # 定义、字段、复制和结构体切片
+    │   └── main.go         # 学生记录与简化测试数据表
+    └── 09_pointers/        # 第 09 课:指针
+        ├── README.md       # 取地址、解引用、nil 和参数传递
+        └── main.go         # 指针修改整数与结构体
 ```
 
 根目录不放例程代码。各课共享根目录的 `go.mod`,不需要逐课初始化模块,也不需要 `go.work`。当前模块路径 `example.com/go-study` 是学习用标识,不必与 Git 地址相同。
@@ -53,6 +56,7 @@
 | 06 | [map 键值对](./lessons/06_maps/README.md) | 增删改查、存在性判断、遍历、nil | 已建立 |
 | 07 | [函数](./lessons/07_functions/README.md) | 参数、返回值、值传递、多返回值、可变参数 | 已建立 |
 | 08 | [结构体](./lessons/08_structs/README.md) | 定义、字段访问、零值、复制、结构体切片 | 已建立 |
+| 09 | [指针](./lessons/09_pointers/README.md) | &、*、nil、指针参数、结构体指针、new | 已建立 |
 
 后续按编号逐步新增独立目录。不一次性铺开后续课程,也不把新例程写进旧例程的入口文件。
 

+ 183 - 0
lessons/09_pointers/README.md

@@ -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)

+ 87 - 0
lessons/09_pointers/main.go

@@ -0,0 +1,87 @@
+package main
+
+import "fmt"
+
+type Student struct {
+	Name string
+	Age  int
+}
+
+// 传入整数副本,只修改函数内部的 value。
+func changeValue(value int) {
+	value = 100
+}
+
+// 指针参数本身也按值传递,但它指向调用方的整数。
+// 返回 bool 表示是否成功,避免解引用 nil 指针。
+func changeByPointer(value *int) bool {
+	if value == nil {
+		return false
+	}
+	*value = 100
+	return true
+}
+
+// 给参数指针重新赋值,不会改变调用方指针本身。
+func redirect(value *int) {
+	local := 999
+	value = &local
+}
+
+// 结构体指针可以直接用点号访问字段,无需 C 风格的 ->。
+func birthday(student *Student) bool {
+	if student == nil {
+		return false
+	}
+	student.Age++ // 等价于 (*student).Age++。
+	return true
+}
+
+func main() {
+	// 1. & 取得变量地址;*int 表示“指向 int 的指针类型”。
+	fmt.Println("=== 取地址与解引用 ===")
+	number := 10
+	var pointer *int = &number
+	fmt.Printf("number 的值:%d\n", number)
+	fmt.Printf("指针类型:%T,是否指向 number:%t\n", pointer, pointer == &number)
+	fmt.Printf("通过 *pointer 读取:%d\n", *pointer)
+
+	// 2. *pointer 在表达式中表示访问所指向的变量。
+	*pointer = 20
+	fmt.Printf("通过指针修改后,number:%d\n", number)
+
+	// 3. 对比普通参数与指针参数。
+	fmt.Println("\n=== 函数修改原变量 ===")
+	changeValue(number)
+	fmt.Printf("普通参数调用后:%d\n", number)
+	ok := changeByPointer(&number)
+	fmt.Printf("指针参数调用后:%d,成功:%t\n", number, ok)
+
+	// 4. 复制指针仍指向同一变量;改指向和改内容是两回事。
+	fmt.Println("\n=== 指针复制与重新指向 ===")
+	alias := pointer
+	*alias = 30
+	fmt.Printf("通过 alias 修改后,number:%d\n", number)
+	redirect(pointer)
+	fmt.Printf("函数内重新指向后,原指针仍指向 number:%t\n", pointer == &number)
+
+	// 5. 指针零值是 nil,没有指向有效变量时不能解引用。
+	fmt.Println("\n=== nil 指针 ===")
+	var empty *int
+	fmt.Printf("空指针为 nil:%t\n", empty == nil)
+	fmt.Printf("传入 nil,修改成功:%t\n", changeByPointer(empty))
+	// fmt.Println(*empty) // 取消注释会导致运行时 panic。
+
+	// 6. 结合结构体:通过指针修改原学生的年龄。
+	fmt.Println("\n=== 结构体指针 ===")
+	student := Student{Name: "小朱", Age: 20}
+	birthday(&student)
+	fmt.Printf("%s 过生日后,年龄:%d\n", student.Name, student.Age)
+
+	// 7. new(int) 返回指向一个零值 int 的指针,不是 nil。
+	fmt.Println("\n=== new 创建指针 ===")
+	created := new(int)
+	fmt.Printf("指针为 nil:%t,初始值:%d\n", created == nil, *created)
+	*created = 42
+	fmt.Printf("修改后的值:%d\n", *created)
+}