Go 语言指向指针的指针(手把手讲解)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

截止目前, 星球 内专栏累计输出 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))  
    // 手动计算并操作地址(慎用)  
}  

六、总结与实践建议

核心要点回顾

  1. 指针是内存地址的“标签”,指向指针的指针是“标签的标签”;
  2. 双指针在函数参数传递、数据结构操作中具有独特优势;
  3. 需谨慎处理指针的初始化与内存生命周期,避免空指针或内存泄漏。

学习路径建议

  1. 先掌握基础指针操作(如 &* 运算符);
  2. 通过修改函数外指针的案例理解双指针的必要性;
  3. 在链表、树等数据结构中实践双指针技巧;
  4. 结合调试工具(如 fmt.Printf("%p"...))观察内存地址变化。

结论

“Go 语言指向指针的指针”并非高深概念,而是通过分层操作实现高效内存管理与复杂逻辑的工具。掌握这一技术后,开发者可以更灵活地处理函数参数传递、动态数据结构操作等场景。建议读者通过编写实际代码(如实现双向链表或自定义内存池)来巩固理解,逐步提升对 Go 内存模型的掌控能力。

通过本文的系统讲解与案例分析,相信读者已能清晰理解多级指针的原理与应用场景,从而在 Go 开发中游刃有余地运用这一技术。

最新发布