Go语言指针基础
什么是指针
简单来说指针是存储另一个变量的内存地址的变量。
在上图中,变量 b 的值被定义为252并存储在内存地址 0x1040a124 上。
变量 a 保存 b 的地址,现在 a 指向 b。
声明指针
package main
import (
"fmt"
)
func main() {
b := 252
var a *int = &b
fmt.Printf("a 的类型为:%T\n", a)
fmt.Println("b的地址为:", a)
}
上述程序我们将 b 的地址分配给类型是 *int 的变量a。现在说 a 指向 b。当我们打印 a 中的值时,将会显示 b 的地址。
a 的类型为:*int
b的地址为: 0xc0000ac1a0
指针的零值
指针的零值是nil。
注:在Go中,除了指针类型,nil 还可以代表其他几种类型的零值,包括切片(slice)、映射(map)、通道(channel)、接口(interface)和函数类型。 对于这些类型,nil 同样表示它们没有被赋予任何具体的值或实例。
package main
import (
"fmt"
)
func main() {
var a *int // or var a = new(int)
var b *[]int
var c *int
var d **int
fmt.Printf("a 的类型为:%T\n", a)
fmt.Printf("b 的类型为:%T\n", b)
fmt.Printf("c 的类型为:%T\n", c)
fmt.Printf("d 的类型为:%T\n", d)
fmt.Println("a的值为:", a)
fmt.Println("b的值为:", b)
fmt.Println("c的值为:", c)
fmt.Println("d的值为:", d)
fmt.Println("a的地址为:", &a)
fmt.Println("b的地址为:", &b)
fmt.Println("c的地址为:", &c)
fmt.Println("d的地址为:", &d)
}
a 的类型为:*int
b 的类型为:*[]int
c 的类型为:*int
d 的类型为:**int
a的值为: <nil>
b的值为: <nil>
c的值为: <nil>
d的值为: <nil>
a的地址为: 0xc00000e038
b的地址为: 0xc00000e040
c的地址为: 0xc00000e048
d的地址为: 0xc00000e050
访问指针变量
如何访问指针指向的变量值?需要在变量前面加上星号 *。 例如:*a 是访问a指向的变量值。
package main
import (
"fmt"
)
func main() {
b := 252
a := &b
fmt.Println("b 的地址是:", a)
fmt.Println("b 的值是:", *a)
*a++
fmt.Println("修改后 b 的地址是:", a)
fmt.Println("修改后 b 的值是:", b)
}
b 的地址是: 0xc0000162e8
b 的值是: 252
修改后 b 的地址是: 0xc0000162e8
修改后 b 的值是: 253
指针作为函数的参数
package main
import (
"fmt"
)
func change(val *int) {
*val = 55
}
func main() {
a := 58
fmt.Println("变量 a 在调用函数之前:", a)
b := &a
change(b)
fmt.Println("变量 a 在调用函数之后:", a)
}
在上面的程序中,我们将a 的地址保存到指针变量 b 中,并将b传递给函数change。在change函数内部,更改 a 的值。因为指针是指向的内存地址,因此函数内部的修改会反应到函数外部的变量中。
变量 a 在调用函数之前: 58
变量 a 在调用函数之后: 55
不支持指针运算
Go 不支持其他语言(如 C 和 C++)中存在的指针运算。
package main
func main() {
b := [...]int{109, 110, 111}
p := &b
p++
}
上面的程序会抛出编译错误
二级指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。这就是我们所说的二级指针。
package main
import (
"fmt"
)
func main() {
var a int
var ptr *int
var pptr **int
a = 3000
/* 指针 ptr 地址 */
ptr = &a
/* 指向指针 ptr 地址 */
pptr = &ptr
/* 获取 pptr 的值 */
fmt.Println("变量 a = ", a)
fmt.Println("变量 a 地址 = ", &a)
fmt.Println("指针 ptr = ", ptr)
fmt.Println("指针变量 ptr 地址 = ", &ptr)
fmt.Println("指针变量 *ptr = ", *ptr)
fmt.Println("指针 pptr = ", pptr)
fmt.Println("指针变量 pptr 地址 = ", &pptr)
fmt.Println("指向指针的指针变量 **pptr = ", **pptr)
}
变量 a = 3000
变量 a 地址 = 0xc0000162e8
指针 ptr = 0xc0000162e8
指针变量 ptr 地址 = 0xc00000e038
指针变量 *ptr = 3000
指针 pptr = 0xc00000e038
指针变量 pptr 地址 = 0xc00000e040
指向指针的指针变量 **pptr = 3000
实战
《合并两个有序链表》
下面我们使用二级指针来实现LeetCode上的《合并两个有序链表》
通过这道题帮助我们深刻立即:
- 函数要改变指针,参数就要是指针的指针
- 函数要改变指针指向的内容,参数只需要指针
func MergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
var head, current *ListNode
for list1 != nil && list2 != nil {
if list2.Val >= list1.Val {
AppendList(&head, ¤t, list1.Val)
list1 = list1.Next
} else {
AppendList(&head, ¤t, list2.Val)
list2 = list2.Next
}
}
if list1 != nil {
current.Next = list1
} else if list2 != nil {
current.Next = list2
}
return head
}
// AppendList 函数用于创建链表,使用二级指针对链表进行修改
func AppendList(headPointer, currentPointer **ListNode, val int) {
newNode := &ListNode{Val: val}
if *headPointer == nil {
*headPointer = newNode
} else {
(*currentPointer).Next = newNode
}
*currentPointer = newNode
}
Golang源码中的二级指针
Golang Map源码的实现中使用二级指针来存放bucket中的key和value值
详情请阅读GO源码分析-map
unsafe.Pointer
通过上述内容我们已知,Go指针无法进行数学运算、比较以及不同类型指针间不能互转和赋值。
unsafe.Pointer提供了一个重要的能力:任何类型的指针和 unsafe.Pointer 可以相互转换。
一个实际应用就是使用unsafe.Pointer实现零拷贝字符串和bytes之间的转换。
func string2bytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&s))
}
func bytes2string(b []byte) string{
return *(*string)(unsafe.Pointer(&b))
}
原理上是利用指针的强转。
参考
Go 语言指针详解
Go 语言多级指针 - 指向指针的指针
Go 语言指针与函数
GO源码分析-map
为什么要用指针,什么时候该用指针,什么时候该用指针的指针