Go 语言指向指针的指针(手把手讲解)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观
前言
在 Go 语言中,指针是连接内存地址与变量值的重要工具。而“指向指针的指针”这一概念,虽然听起来抽象,却是处理复杂数据结构或需要间接操作内存场景时的关键技术。对于编程初学者而言,多级指针可能带来认知挑战,但通过循序渐进的讲解和实际案例,可以轻松掌握其核心逻辑。本文将从基础到进阶,结合比喻和代码示例,帮助读者彻底理解这一概念。
一、指针的基础概念:从变量到地址
变量与内存地址
在 Go 中,每个变量都对应一个内存地址。例如:
var a int = 10
此时,变量 a
的值为 10
,而 &a
表示该变量的内存地址(例如 0x400000
)。指针就是存储这个地址的变量,其类型为 *T
(T 是具体类型)。
比喻:可以将指针想象为一张“地址标签”,它记录了某个具体值的存放位置。
var a int = 10
var ptr *int = &a // ptr 是指向 a 的指针
指针的解引用与修改
通过 *
运算符,可以访问指针指向的值:
fmt.Println(*ptr) // 输出 10
*ptr = 20
fmt.Println(a) // 输出 20
此时,通过修改指针 ptr
所指向的值,原变量 a
的值也被间接改变。
二、指向指针的指针:地址的地址
什么是“指向指针的指针”?
当一个指针的类型是 **T
时,它就是指向指针的指针。其作用是存储另一个指针的地址,从而实现对指针本身的间接操作。
比喻:
- 指针是“地址标签”;
- 指向指针的指针就是“地址标签的标签”,它指向的是另一个标签的存放位置。
var a int = 10
var ptr *int = &a
var ptrToPtr **int = &ptr // ptrToPtr 指向 ptr 的地址
核心操作与示例
1. 访问第三层值
通过两次解引用,可以获取原始变量的值:
fmt.Println(**ptrToPtr) // 输出 10
2. 修改底层指针的指向
var b int = 20
*ptrToPtr = &b // 修改 ptr 的地址为 b 的地址
fmt.Println(*ptr) // 输出 20
此时,ptr
本身被指向了新的地址,而 a
的值未被修改。
代码示例:多级指针的链式操作
package main
import "fmt"
func main() {
var x int = 100
var ptr *int = &x
var ptrToPtr **int = &ptr
fmt.Println("原始值:", x) // 输出 100
fmt.Println("通过指针访问:", *ptr) // 输出 100
fmt.Println("通过双指针访问:", **ptrToPtr) // 输出 100
// 修改底层指针指向
var y int = 200
*ptrToPtr = &y
fmt.Println("修改后 x:", x) // 输出 100(未变)
fmt.Println("修改后 *ptr:", *ptr) // 输出 200
}
三、为什么需要指向指针的指针?
场景 1:函数中修改外部指针
在 Go 中,函数参数按值传递。若想在函数内修改外部指针的指向,需传入指向指针的指针:
func updatePointer(pptr **int, newValue *int) {
*pptr = newValue // 修改外部指针的地址
}
func main() {
var x int = 10
var ptr *int = &x
var y int = 20
updatePointer(&ptr, &y) // 传入 **int 类型
fmt.Println(*ptr) // 输出 20
}
场景 2:复杂数据结构的遍历
在链表或树结构中,双指针技术常用于高效操作节点。例如,反转链表时:
type ListNode struct {
Val int
Next *ListNode
}
func reverseList(head **ListNode) {
var prev *ListNode = nil
curr := *head
for curr != nil {
nextTemp := curr.Next
curr.Next = prev
prev = curr
curr = nextTemp
}
*head = prev // 通过双指针更新原始 head
}
func main() {
// 构造链表 1 -> 2 -> 3
head := &ListNode{1, &ListNode{2, &ListNode{3, nil}}}
reverseList(&head)
// 此时 head 指向反转后的链表 3 -> 2 -> 1
}
场景 3:避免重复的指针分配
在需要动态调整指针指向的场景中,双指针能减少中间变量的使用:
var arr = []int{1, 2, 3}
var ptr **int = &(&arr[0]) // 指向数组首元素的指针
for i := 0; i < len(arr); i++ {
fmt.Println(**ptr) // 输出 1, 2, 3
ptr = &(*ptr)[1] // 直接修改指针指向下一个元素
}
四、常见误区与调试技巧
误区 1:混淆多级指针的解引用层次
var a int = 10
var ptr *int = &a
var ptrToPtr **int = &ptr
fmt.Println(ptrToPtr) // 输出地址值,如 0x400000
fmt.Println(*ptrToPtr) // 输出 10 的地址(如 0x400010)
fmt.Println(**ptrToPtr) // 输出 10
若误用单次解引用,可能导致类型错误或读取无效地址。
误区 2:未初始化指针
var ptr *int // 未初始化,值为 nil
var ptrToPtr **int = &ptr // 正确,ptr 存在但未指向有效内存
*ptrToPtr = new(int) // 正确,分配内存并修改 ptr 的指向
但若直接:
var p **int = new(**int) // 错误,分配的是 **int 类型的内存,但未初始化底层指针
此时需通过 *p = new(int)
完成初始化。
调试技巧:打印指针地址
使用 %p
格式化动词可直接查看内存地址:
fmt.Printf("ptr 地址: %p\n", &ptr)
fmt.Printf("ptrToPtr 值: %p\n", ptrToPtr) // 等同于 &ptr 的值
五、进阶应用:双指针与内存管理
1. 内存泄漏的预防
在动态分配内存后,通过双指针操作需确保及时释放:
func example() {
var p **int = new(**int) // 分配内存
*p = new(int) // 分配底层指针的内存
defer func() {
delete(*p) // 假设存在释放函数
delete(p)
}()
}
2. 与 unsafe 包的结合
在需要直接操作内存地址的场景中,unsafe
包可与多级指针配合使用:
import "unsafe"
func main() {
var a int = 42
ptr := &a
ptrToPtr := &ptr
// 获取双指针的原始内存地址
addr := uintptr(unsafe.Pointer(ptrToPtr))
// 手动计算并操作地址(慎用)
}
六、总结与实践建议
核心要点回顾
- 指针是内存地址的“标签”,指向指针的指针是“标签的标签”;
- 双指针在函数参数传递、数据结构操作中具有独特优势;
- 需谨慎处理指针的初始化与内存生命周期,避免空指针或内存泄漏。
学习路径建议
- 先掌握基础指针操作(如
&
和*
运算符); - 通过修改函数外指针的案例理解双指针的必要性;
- 在链表、树等数据结构中实践双指针技巧;
- 结合调试工具(如
fmt.Printf("%p"...)
)观察内存地址变化。
结论
“Go 语言指向指针的指针”并非高深概念,而是通过分层操作实现高效内存管理与复杂逻辑的工具。掌握这一技术后,开发者可以更灵活地处理函数参数传递、动态数据结构操作等场景。建议读者通过编写实际代码(如实现双向链表或自定义内存池)来巩固理解,逐步提升对 Go 内存模型的掌控能力。
通过本文的系统讲解与案例分析,相信读者已能清晰理解多级指针的原理与应用场景,从而在 Go 开发中游刃有余地运用这一技术。