1. 什么是指针
指针是编程中一个重要的概念,它就像一个"地址标签",存储着内存中某个变量的具体位置信息。通过指针,我们可以直接访问和操作内存中的数据。
1.1 指针的基本概念
想象一下现实生活中的场景:
- 你的家有一个具体地址(比如:北京市朝阳区xxx街道xxx号)
- 别人想要找到你家,只需要知道这个地址就可以了
- 指针就相当于这个"地址",它存储的是变量在内存中的位置
在计算机中:
- 每个变量都会被分配一块内存空间来存储数据
- 这块内存空间有一个唯一的地址
- 指针变量存储的就是这个内存地址
1.2 为什么需要指针
指针主要有以下几个重要作用:
- 节省内存:传递大型数据结构时,传递指针比传递整个数据更高效
- 直接修改:可以在函数中直接修改原始数据
- 动态内存管理:可以动态地分配和释放内存
- 数据结构实现:链表、树等复杂数据结构的实现基础
2. 指针的声明和使用
2.1 指针的声明
go
// 指针的声明语法:var 指针变量名 *数据类型
var ptr *int // 声明一个指向int类型的指针
var ptr2 *string // 声明一个指向string类型的指针
var ptr3 *float64 // 声明一个指向float64类型的指针
2.2 获取变量地址(&操作符)
go
package main
import "fmt"
func main() {
// 声明普通变量
var num int = 100
var name string = "张三"
var score float64 = 95.5
// 使用&操作符获取变量的内存地址
var ptrNum *int = &num
var ptrName *string = &name
var ptrScore *float64 = &score
fmt.Printf("变量num的值:%d,地址:%p\n", num, &num)
fmt.Printf("变量name的值:%s,地址:%p\n", name, &name)
fmt.Printf("变量score的值:%f,地址:%p\n", score, &score)
fmt.Printf("指针ptrNum存储的地址:%p,指向的值:%d\n", ptrNum, *ptrNum)
fmt.Printf("指针ptrName存储的地址:%p,指向的值:%s\n", ptrName, *ptrName)
fmt.Printf("指针ptrScore存储的地址:%p,指向的值:%f\n", ptrScore, *ptrScore)
}
运行结果:
bash
变量num的值:100,地址:0xc0000140a8
变量name的值:张三,地址:0xc000010230
变量score的值:95.500000,地址:0xc0000140b8
指针ptrNum存储的地址:0xc0000140a8,指向的值:100
指针ptrName存储的地址:0xc000010230,指向的值:张三
指针ptrScore存储的地址:0xc0000140b8,指向的值:95.500000
2.3 访问指针指向的值(*操作符)
go
package main
import "fmt"
func main() {
var num int = 50
var ptr *int = &num // ptr指向num的地址
fmt.Printf("num的值:%d\n", num)
fmt.Printf("ptr存储的地址:%p\n", ptr)
fmt.Printf("通过指针访问的值:%d\n", *ptr) // *ptr表示获取ptr指向地址处的值
// 通过指针修改值
*ptr = 100
fmt.Printf("修改后num的值:%d\n", num) // num的值也被修改了
fmt.Printf("通过指针访问修改后的值:%d\n", *ptr)
}
运行结果:
bash
num的值:50
ptr存储的地址:0xc0000140a8
通过指针访问的值:50
修改后num的值:100
通过指针访问修改后的值:100
3. 指针的零值和nil
3.1 指针的零值
go
package main
import "fmt"
func main() {
// 声明指针变量但不初始化
var ptr *int
var ptr2 *string
fmt.Printf("未初始化的int指针:%v,是否为nil:%t\n", ptr, ptr == nil)
fmt.Printf("未初始化的string指针:%v,是否为nil:%t\n", ptr2, ptr2 == nil)
// 尝试访问未初始化指针的值会导致panic
// fmt.Println(*ptr) // 这行代码会报错!
}
运行结果:
bash
未初始化的int指针:<nil>,是否为nil:true
未初始化的string指针:<nil>,是否为nil:true
3.2 nil指针的安全检查
go
package main
import "fmt"
func main() {
var ptr *int
// 安全检查
if ptr != nil {
fmt.Println("指针不为nil,可以安全访问")
fmt.Println(*ptr)
} else {
fmt.Println("指针为nil,不能访问")
}
// 初始化指针后
num := 42
ptr = &num
if ptr != nil {
fmt.Println("指针不为nil,可以安全访问")
fmt.Println("指针指向的值:", *ptr)
}
}
运行结果:
bash
指针为nil,不能访问
指针不为nil,可以安全访问
指针指向的值: 42
4. 指针与函数
4.1 指针作为函数参数
go
package main
import "fmt"
// 传值方式 - 不会修改原始值
func changeValueByValue(x int) {
x = 100
fmt.Printf("函数内x的值:%d\n", x)
}
// 传指针方式 - 可以修改原始值
func changeValueByPointer(ptr *int) {
*ptr = 200
fmt.Printf("函数内通过指针修改的值:%d\n", *ptr)
}
func main() {
num := 50
fmt.Printf("调用函数前num的值:%d\n", num)
// 传值调用
changeValueByValue(num)
fmt.Printf("传值调用后num的值:%d\n", num)
// 传指针调用
changeValueByPointer(&num)
fmt.Printf("传指针调用后num的值:%d\n", num)
}
运行结果:
bash
调用函数前num的值:50
函数内x的值:100
传值调用后num的值:50
函数内通过指针修改的值:200
传指针调用后num的值:200
4.2 返回指针的函数
go
package main
import "fmt"
// 返回指向int的指针
func createIntPointer(value int) *int {
return &value
}
// 返回指向结构体的指针
func createStudentPointer(name string, age int) *struct {
Name string
Age int
} {
student := struct {
Name string
Age int
}{name, age}
return &student
}
func main() {
// 获取指针
ptr := createIntPointer(999)
fmt.Printf("指针地址:%p,指向的值:%d\n", ptr, *ptr)
// 修改通过指针访问的值
*ptr = 888
fmt.Printf("修改后的值:%d\n", *ptr)
// 结构体指针
studentPtr := createStudentPointer("李四", 20)
fmt.Printf("学生信息:%s,%d岁\n", (*studentPtr).Name, (*studentPtr).Age)
// Go语言提供了简化语法
fmt.Printf("学生信息简化写法:%s,%d岁\n", studentPtr.Name, studentPtr.Age)
}
运行结果:
bash
指针地址:0xc0000140a8,指向的值:999
修改后的值:888
学生信息:李四,20岁
学生信息简化写法:李四,20岁
5. 指针数组和数组指针
5.1 指针数组
go
package main
import "fmt"
func main() {
// 创建几个变量
a, b, c := 1, 2, 3
// 指针数组 - 数组的每个元素都是指针
ptrArray := [3]*int{&a, &b, &c}
fmt.Println("指针数组的内容:")
for i, ptr := range ptrArray {
fmt.Printf("索引%d:地址%p,值%d\n", i, ptr, *ptr)
}
// 通过指针数组修改原变量
*ptrArray[0] = 10
*ptrArray[1] = 20
*ptrArray[2] = 30
fmt.Printf("修改后:a=%d, b=%d, c=%d\n", a, b, c)
}
运行结果:
bash
指针数组的内容:
索引0:地址0xc0000140a8,值1
索引1:地址0xc0000140b0,值2
索引2:地址0xc0000140b8,值3
修改后:a=10, b=20, c=30
5.2 数组指针
go
package main
import "fmt"
func main() {
// 普通数组
arr := [3]int{10, 20, 30}
// 数组指针 - 指向整个数组的指针
arrPtr := &arr
fmt.Printf("数组地址:%p\n", &arr)
fmt.Printf("数组指针存储的地址:%p\n", arrPtr)
fmt.Printf("通过数组指针访问元素:%d, %d, %d\n",
(*arrPtr)[0], (*arrPtr)[1], (*arrPtr)[2])
// 修改数组元素
(*arrPtr)[0] = 100
(*arrPtr)[1] = 200
(*arrPtr)[2] = 300
fmt.Printf("修改后数组:%v\n", arr)
// Go语言的简化写法
arrPtr[0] = 1000 // 等价于 (*arrPtr)[0] = 1000
fmt.Printf("简化写法修改后:%v\n", arr)
}
运行结果:
bash
数组地址:0xc000016060
数组指针存储的地址:0xc000016060
通过数组指针访问元素:10, 20, 30
修改后数组:[100 200 300]
简化写法修改后:[1000 200 300]
6. 指针的指针(多级指针)
go
package main
import "fmt"
func main() {
num := 42
// 一级指针
ptr1 := &num
fmt.Printf("num值:%d,地址:%p\n", num, &num)
fmt.Printf("一级指针ptr1:%p,指向的值:%d\n", ptr1, *ptr1)
// 二级指针(指向指针的指针)
ptr2 := &ptr1
fmt.Printf("二级指针ptr2:%p,指向的地址:%p,最终值:%d\n",
ptr2, *ptr2, **ptr2)
// 三级指针
ptr3 := &ptr2
fmt.Printf("三级指针ptr3:%p,最终值:%d\n", ptr3, ***ptr3)
// 通过多级指针修改值
***ptr3 = 999
fmt.Printf("通过三级指针修改后num的值:%d\n", num)
}
运行结果:
bash
num值:42,地址:0xc0000140a8
一级指针ptr1:0xc0000140a8,指向的值:42
二级指针ptr2:0xc000006028,指向的地址:0xc0000140a8,最终值:42
三级指针ptr3:0xc000006038,最终值:42
通过三级指针修改后num的值:999
7. 指针的实际应用示例
7.1 交换两个数的值
go
package main
import "fmt"
// 使用指针交换两个数
func swap(a, b *int) {
temp := *a
*a = *b
*b = temp
}
func main() {
x, y := 10, 20
fmt.Printf("交换前:x=%d, y=%d\n", x, y)
swap(&x, &y)
fmt.Printf("交换后:x=%d, y=%d\n", x, y)
}
运行结果:
bash
交换前:x=10, y=20
交换后:x=20, y=10
7.2 链表节点示例
go
package main
import "fmt"
// 定义链表节点结构
type ListNode struct {
Value int
Next *ListNode
}
// 创建新节点
func NewNode(value int) *ListNode {
return &ListNode{Value: value, Next: nil}
}
// 在链表末尾添加节点
func AppendNode(head **ListNode, value int) {
newNode := NewNode(value)
if *head == nil {
*head = newNode
return
}
current := *head
for current.Next != nil {
current = current.Next
}
current.Next = newNode
}
// 打印链表
func PrintList(head *ListNode) {
current := head
for current != nil {
fmt.Printf("%d -> ", current.Value)
current = current.Next
}
fmt.Println("nil")
}
func main() {
var head *ListNode = nil
// 添加节点
AppendNode(&head, 1)
AppendNode(&head, 2)
AppendNode(&head, 3)
AppendNode(&head, 4)
fmt.Println("链表内容:")
PrintList(head)
}
运行结果:
bash
链表内容:
1 -> 2 -> 3 -> 4 -> nil
8. 指针注意事项和最佳实践
8.1 注意事项
- 空指针访问危险:访问nil指针会导致程序崩溃
- 野指针问题:指向已释放内存的指针
- 内存泄漏:忘记释放动态分配的内存
8.2 最佳实践
go
package main
import "fmt"
func main() {
// 1. 始终检查指针是否为nil
var ptr *int
if ptr != nil {
fmt.Println(*ptr) // 安全访问
}
// 2. 初始化指针
num := 100
ptr = &num
fmt.Printf("安全访问:%d\n", *ptr)
// 3. 使用指针时要考虑生命周期
data := make([]int, 5)
ptrToSlice := &data
fmt.Printf("切片指针:%v\n", *ptrToSlice)
// 4. 函数返回指针时要注意
func returnPointer() *int {
value := 42 // 局部变量
return &value // 危险!返回局部变量的地址
}
// 正确的做法
func returnPointerSafe() *int {
value := new(int) // 在堆上分配内存
*value = 42
return value
}
}
9. new函数和make函数的区别
9.1 new函数
go
package main
import "fmt"
func main() {
// new函数为指定类型分配零值内存并返回指针
ptr1 := new(int) // *int类型,值为0
ptr2 := new(string) // *string类型,值为""
ptr3 := new([3]int) // *[3]int类型,值为[0,0,0]
fmt.Printf("int指针:%p,值:%d\n", ptr1, *ptr1)
fmt.Printf("string指针:%p,值:%s\n", ptr2, *ptr2)
fmt.Printf("数组指针:%p,值:%v\n", ptr3, *ptr3)
// 修改值
*ptr1 = 100
*ptr2 = "Hello"
(*ptr3)[0] = 1
(*ptr3)[1] = 2
(*ptr3)[2] = 3
fmt.Printf("修改后int值:%d\n", *ptr1)
fmt.Printf("修改后string值:%s\n", *ptr2)
fmt.Printf("修改后数组值:%v\n", *ptr3)
}
运行结果:
bash
int指针:0xc0000140a8,值:0
string指针:0xc000010230,值:
数组指针:0xc000016060,值:[0 0 0]
修改后int值:100
修改后string值:Hello
修改后数组值:[1 2 3]
9.2 make函数
go
package main
import "fmt"
func main() {
// make函数用于创建切片、map、channel
slice := make([]int, 5) // 创建长度为5的切片
mapData := make(map[string]int) // 创建map
ch := make(chan int) // 创建channel
fmt.Printf("切片:%v,长度:%d\n", slice, len(slice))
fmt.Printf("map:%v\n", mapData)
fmt.Printf("channel:%v\n", ch)
// 初始化数据
slice[0] = 10
slice[1] = 20
mapData["one"] = 1
mapData["two"] = 2
fmt.Printf("初始化后切片:%v\n", slice)
fmt.Printf("初始化后map:%v\n", mapData)
}
10. 总结
指针是Golang中一个强大但需要谨慎使用的特性:
核心概念:
- 指针存储的是内存地址,而不是具体的值
- 使用
&获取变量地址,使用*访问指针指向的值 - 指针的零值是
nil
主要用途:
- 函数间共享和修改数据
- 避免大数据结构的拷贝开销
- 实现复杂数据结构(链表、树等)
- 动态内存管理
注意事项:
- 始终检查指针是否为nil
- 注意变量的生命周期
- 避免悬空指针和内存泄漏
掌握指针对于深入理解Golang和编写高效的程序非常重要,虽然初学时可能会觉得复杂,但通过大量练习就能熟练运用。