对话:指针的本质 - 不过是一个存地址的变量
前置知识: [01-virtual-memory.md, 02-memory-layout.md] 难度: ⭐⭐ 预计时间: 35分钟 关键词: 指针, 地址, 解引用, 值传递, 引用传递, unsafe.Pointer
引入场景
很多人觉得指针难理解、容易出错。但指针的本质其实极其简单——它就是一个存储内存地址的变量。学完虚拟内存和内存布局后,指针就不再神秘了。
对话探索
问题1:指针到底是什么?它和普通变量有什么区别?
💭 思考方向:
var x int = 42在内存中是什么样子?var p *int = &x在内存中又是什么样子?- p本身也占内存空间,它存的是什么?
📖 参考答案:
内存(虚拟地址空间)
地址 内容
─────────────────────
0xC0000A0 42 ← x 变量,存的是值
...
0xC0000B0 0xC0000A0 ← p 变量,存的是 x 的地址
指针就是一个变量,它的值是另一个变量的地址。
var x int = 42 // x在地址0xC0000A0,内容是42
var p *int = &x // p在地址0xC0000B0,内容是0xC0000A0
fmt.Println(x) // 42(直接读取x的值)
fmt.Println(p) // 0xC0000A0(p存的是地址)
fmt.Println(*p) // 42(解引用:去p存的地址处读取值)三个关键操作:
| 操作 | 语法 | 含义 |
|---|---|---|
| 取地址 | &x | 获取变量x的内存地址 |
| 声明指针 | var p *int | 声明一个存储int地址的变量 |
| 解引用 | *p | 去p存的地址处读/写值 |
问题2:Go是”值传递”还是”引用传递”?函数内修改参数会影响外面吗?
💭 思考方向:
- 函数参数是复制一份还是传地址?
- 传指针和传值有什么区别?
- slice、map呢?它们好像能在函数内修改?
📖 参考答案: Go一切都是值传递——但传递的”值”可以是一个指针(地址)。
// 传值:函数内修改不影响外面
func changeValue(x int) {
x = 100 // 修改的是x的副本
}
// 传指针:可以修改原始值
func changePointer(p *int) {
*p = 100 // 通过地址修改原始值
}
func main() {
a := 42
changeValue(a)
fmt.Println(a) // 42(未改变)
changePointer(&a)
fmt.Println(a) // 100(改变了)
}传值时: 传指针时:
main栈帧 main栈帧
┌────────┐ ┌────────┐
│ a = 42 │ │ a = 42 │ ← *p 通过地址直接修改这里
└────────┘ └────────┘
↑
changeValue栈帧 changePointer栈帧
┌────────┐ ┌─────────────┐
│ x = 42 │ │ p = &a(地址) │
│→x = 100│ │ *p = 100 │
└────────┘ └─────────────┘
x是副本, p是指针的副本,
修改不影响a 但指向同一个a
slice、map的”特殊”行为:
func modify(s []int) {
s[0] = 100 // 能修改!但不是因为"引用传递"
}slice的底层结构是:
type slice struct {
ptr *array // 指向底层数组的指针
len int
cap int
}传递slice时,复制了这个结构体(值传递)。但 ptr 字段的副本仍然指向同一个底层数组——所以修改元素会影响原始slice。
问题3:new 和 make 有什么区别?什么时候用哪个?
💭 思考方向:
new(int)返回什么?make([]int, 10)返回什么?- 为什么slice不能用new?
📖 参考答案:
new(T) | make(T, ...) | |
|---|---|---|
| 返回值 | *T(指针) | T(值) |
| 初始化 | 零值 | 完整初始化 |
| 适用类型 | 所有类型 | 仅 slice、map、channel |
| 做了什么 | 分配内存,填零值 | 分配+初始化内部结构 |
// new:分配零值,返回指针
p := new(int) // p 是 *int,*p == 0
s := new([]int) // s 是 *[]int,*s == nil(没有底层数组!)
// make:分配并初始化内部结构
s2 := make([]int, 10) // s2 是 []int,len=10,底层数组已分配
m := make(map[string]int) // m 是 map,内部哈希表已初始化
ch := make(chan int, 5) // ch 是 channel,缓冲区已分配为什么slice/map/channel需要make?因为它们内部有复杂结构(指针+长度+容量),仅分配零值是不够的。
问题4:Go有 unsafe.Pointer,它和普通指针有什么不同?为什么叫”unsafe”?
💭 思考方向:
- 普通指针有类型约束(
*int只能指向int) - 如果想把
*int转成*float64呢? - C语言的
void*是什么?
📖 参考答案:
// 普通指针:类型安全
var x int = 42
var p *int = &x
// var q *float64 = p ← 编译错误!类型不匹配
// unsafe.Pointer:绕过类型系统
var q *float64 = (*float64)(unsafe.Pointer(&x))
fmt.Println(*q) // 输出 x 的位模式按 float64 解释的值unsafe.Pointer 的转换规则:
*T ←→ unsafe.Pointer ←→ *U (任意指针类型互转)
unsafe.Pointer ←→ uintptr (指针和整数互转)
为什么叫unsafe?
- 绕过类型检查:编译器不再保证内存安全
- 可能读到垃圾数据:把int的位模式当float64读,结果毫无意义
- GC不感知:uintptr只是整数,GC不知道它是指针,可能移动了对象但不更新uintptr
- 破坏可移植性:依赖内存布局,不同架构可能不同
何时使用:
- 几乎不需要。标准库内部用(如reflect、sync.Pool)
- 性能极度敏感的场景(避免接口转换开销)
- 和C代码交互(CGO)
问题5:nil指针是什么?解引用nil指针为什么会panic?
💭 思考方向:
- nil在内存中是什么值?
- 地址0处有什么?
- 操作系统如何防止访问地址0?
📖 参考答案:
var p *int = nil // p 的值是 0(地址0)
fmt.Println(*p) // panic: runtime error: invalid memory addressnil指针的值就是0——它指向虚拟地址空间的最底部。
操作系统故意将地址0附近的页设为”不可访问”(保护页)。当程序试图访问这些地址时:
- CPU发出内存访问请求
- MMU查页表 → 该页是不可访问的
- CPU触发段错误(Segmentation Fault)
- Go运行时捕获信号,转为panic
虚拟地址空间
0x00000000 ┌───────────┐
│ 保护页 │ ← nil指针指向这里
│ (不可访问) │ 访问就触发段错误
0x00001000 ├───────────┤
│ .text │ ← 程序代码从这里开始
│ ... │
这也是为什么Go中要养成检查nil的习惯:
func process(user *User) {
if user == nil {
return // 防御性编程
}
fmt.Println(user.Name)
}Go语言实践
package main
import (
"fmt"
"unsafe"
)
type Point struct {
X int
Y int
}
func main() {
// 基本指针操作
x := 42
p := &x
fmt.Printf("x的值: %d\n", x)
fmt.Printf("x的地址: %p\n", &x)
fmt.Printf("p的值: %p(就是x的地址)\n", p)
fmt.Printf("p的地址: %p(p自己也在内存中)\n", &p)
fmt.Printf("*p的值: %d(解引用,读取x的值)\n", *p)
// 指针大小
fmt.Printf("\n指针大小: %d字节(64位系统=8字节)\n", unsafe.Sizeof(p))
// 结构体指针
pt := &Point{X: 10, Y: 20}
fmt.Printf("\nPoint地址: %p\n", pt)
fmt.Printf("Point.X: %d\n", pt.X) // Go自动解引用,不需要写(*pt).X
// slice的共享底层数组
a := []int{1, 2, 3}
b := a // b和a共享底层数组
b[0] = 100
fmt.Printf("\na[0] = %d(被b修改了!共享底层数组)\n", a[0])
}小结
⭐ 核心要点:
- 指针 = 存地址的变量,取地址(&)、解引用(*)是两个基本操作
- Go一切值传递,传指针可以修改原始值;slice/map看起来像引用是因为内部有指针
- nil = 地址0,操作系统设置了保护页,访问地址0会触发段错误→panic