在计算机内存中,每个变量都有一个唯一的地址,指针就是用来保存这个地址的变量。通过指针,我们可以间接地访问和修改存储在该地址处的数据。今天我们来聊一聊Java和Go指针,预告一下,我们需要借助C语言做一些小小的比较。
Java
在Java中,不存在直接的指针概念,而是通过引用来访问对象。Java中的对象是通过引用来操作的,这些引用本质上是对对象的引用,而不是指向内存地址的指针。Java的引用是一种高级抽象,它隐藏了底层内存管理的细节,开发者不需要关心对象的内存分配和释放。Java中的引用可以被认为是一种安全的指针,它提供了更高的抽象级别,可以减少内存管理错误的发生。其实大家第一门编程语言应该就是大一学习的C语言编程吧。当时学习指针的时候很懵,尤其是各种指针运算。后来学Java的对象以及引用再回来看就很好理解。
C
Student *student = (Student*)malloc(sizeof(Student));
Java
Student student = new Student();
他们的本质是一样的,都是指向堆空间的某一块地址。之后再回去写C语言,就越写越顺,每次创建变量之前都问自己一句,在栈里还是堆里? 然后决定要不要用指针。
话说回来,sun.misc.Unsafe类提供了一种机制来进行一些底层的、不安全的操作,包括直接操作内存和执行不受限制的任意指针算术运算等。
sun.misc.Unsafe类并不是 Java 标准 API 的一部分,因此它并不受到 Java 平台的正式支持,并且可能在未来的 Java 版本中被移除或更改。它主要被用来实现 Java 核心类库和一些 Java 虚拟机的实现。
通过sun.misc.Unsafe类,你可以直接进行一些底层的内存操作,比如:
- 分配内存
- 释放内存
- 修改内存中的值
- 进行指针算术运算等
使用sun.misc.Unsafe类需要谨慎,因为它涉及到底层的内存操作,可能会导致不稳定性和不可预测的结果。此外,由于它不是 Java 标准 API 的一部分,因此在不同的 Java 实现中,它的行为可能会有所不同。
在 Java 9 中,sun.misc.Unsafe类的一些方法被标记为不安全,并且在一些场景下会抛出 java.lang.UnsupportedOperationException 异常,这是为了增强 Java 应用程序的安全性。
总的来说,除非你对 Java 的内存模型和底层运行机制非常了解,并且对使用 sun.misc.Unsafe 类的风险有所认识,并且确实需要进行底层的内存操作,否则不建议使用 sun.misc.Unsafe 类。Java部分就到此结束,毕竟不是重点。
Go 中的指针
Go语言支持指针,但是和C语言中的指针还是有些不同。在Go中,指针是一种数据类型,它指向了一个内存地址,允许你直接访问内存中的数据。与C语言不同的是,Go语言的指针是类型安全的,不允许进行指针运算,从而减少了一些常见的指针错误。
在Go中,你可以通过使用操作符来声明指针变量,使用&操作符来获取变量的地址,使用操作符来获取指针指向的值。
依稀记得当时学习C语言的时候老师引出的指针的第一个例子:将两个int数传入一个方法,希望这两个int互换数值。这里我用Go来写:
package main
import "fmt"
// 定义一个交换函数
func swap(x, y int) {
var i int = x
x = y
y = i
}
func main() {
// 定义两个整数变量
a, b := 5, 10
fmt.Println("Before swapping:")
fmt.Println("a =", a)
fmt.Println("b =", b)
// 调用交换函数,并接收返回值
swap(a, b)
fmt.Println("After swapping:")
fmt.Println("a =", a)
fmt.Println("b =", b)
}
结果显而易见,我们失败了。然后老师开始长篇大论的解释,然后我就睡着了。其实现在回来再想想很简单,x和y的变量在main的栈帧里面,调用swap方法无非就是创建新的栈帧,并把xy的数值复制一边传递出去。抓捕周树人和我鲁迅有什么关系?
那正确写法就是搞一个指向栈内存的指针。
package main
import "fmt"
// 定义一个交换函数
func swap(x, y *int) {
temp := *x
*x = *y
*y = temp
}
func main() {
// 定义两个整数变量
a, b := 5, 10
fmt.Println("Before swapping:")
fmt.Println("a =", a)
fmt.Println("b =", b)
// 调用交换函数
swap(&a, &b)
fmt.Println("After swapping:")
fmt.Println("a =", a)
fmt.Println("b =", b)
}
接下来我们再看看数组和指针碰到一起会发生什么
指针和数组
指针指向数组中的某一个元素
var arr [5]int
var ptr *int
ptr = &arr[0] // 将指针指向数组的第一个元素
指针指向数组整体
// 声明一个数组
var arr [5]int
// 声明一个指向数组的指针
var ptr *[5]int
// 将指针指向数组
ptr = &arr
这里要注意类型匹配,我们之前说过长度也是类型的一部分,[5]int和[7]int是不一样的。[5]int要配合*[5]int
眼神要好
var ptrArr *[3]int //指向一个数组
var ptrArr [3]*int //每个元素指向一个数
传递数组
之前我们说过,在方法间传递数组其实是复制整个数组传递过去,相当于寻觅另外一个地方盖一座一模一样的房子。如果我们传递指向数组的指针,函数将能够修改原始数组的值。
func modifyArray(arr *[5]int) {
// 修改数组的值
(*arr)[0] = 100
}
func main() {
var arr [5]int
modifyArray(&arr)
fmt.Println(arr) // 输出 [100 0 0 0 0]
}
指针和切片
之前我们已经学过,切片其实就是对数组进行包装,内部通过指针对数组进行操作。那么当切片和指针一起使用时,可以实现更灵活和高效的数据操作,比如动态地管理内存和访问数组的部分元素(二级指针嘛,玩C的都懂)。以下是一些示例,演示了切片和指针的结合使用:
package main
import "fmt"
func main() {
// 创建一个切片
slice := []int{1, 2, 3, 4, 5}
// 创建一个指向切片的指针
var ptr *[]int
ptr = &slice
// 修改切片的值
(*ptr)[0] = 100
(*ptr)[1] = 200
// 打印修改后的切片
fmt.Println(*ptr) // 输出: [100 200 3 4 5]
}
个人觉得切片指针反而比数组用起来更轻松。map也一样道理:
package main
import "fmt"
func modifyMap(m *map[string]int) {
// 向 map 中添加新的键值对
(*m)["d"] = 4
(*m)["e"] = 5
// 修改已有键的值
(*m)["a"] = 100
}
func main() {
// 创建一个 map
myMap := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
// 创建指向 map 的指针
ptr := &myMap
// 修改 map 内容
modifyMap(ptr)
// 打印修改后的 map
fmt.Println(*ptr)
}
指针和结构体
在 Go 语言中,指向结构体的指针是一种非常常见的用法基本和C语言一样。结构体是一种用户自定义的复合数据类型,它可以包含多个不同类型的字段,而指向结构体的指针则允许我们直接访问结构体的字段,并且可以在函数之间共享结构体的实例,而不需要进行复制。以下是指向结构体的指针如何使用的详细说明:
创建结构体指针
// 定义结构体
type Person struct {
Name string
Age int
}
func main() {
// 创建结构体的指针
var p *Person
p = &Person{"John", 30}
// 或者使用 new() 函数创建结构体的指针
p = new(Person)
p.Name = "Alice"
p.Age = 25
}
可以理解这里的new就相当于(*Person)malloc(sizeof(Person));
操作结构体字段
访问结构体字段:
fmt.Println((*p).Name) // 打印结构体字段 Name
fmt.Println(p.Age) // 也可以直接使用 p.Age 访问
修改结构体字段:
p.Name = "Bob" // 直接赋值修改结构体字段
p.Age = 40
传递结构体指针给函数:
func modifyPerson(p *Person) {
if p != nil {
p.Age = 50
}
}
modifyPerson(p) // 调用函数修改结构体的字段值
总的来说,指向结构体的指针在 Go 中是非常常见的用法,它允许我们在函数间共享结构体的实例,并且可以在需要时直接访问和修改结构体的字段。使用指向结构体的指针可以避免结构体的复制,提高程序的性能和效率。
总结
Java中的指针是被隐藏的,程序员无法直接操作内存地址,而Go中的指针是一等公民,允许直接操作内存地址。本文仅仅介绍了指针操作的冰山一角,之后我们会继续介绍指针在面向对象编程中的应用。