🧨🧧🧨🧧🧨:首先祝大家新年快乐!

一、什么是指针(Pointer)

在计算机科学中,指针(英语:Pointer),是编程语言中的一类数据类型及其对象或变量,用来表示或存储一个存储器地址,这个地址的值直接指向(points to)存在该地址的对象的值。【 维基百科

二、我理解的指针

了解磁盘系统的同学可能听说过inode这个东西(这里具体讲了inode),它是Unix文件系统中用来储存文件元信息的东西。

在文件系统中,每个文件对象都对应着一个 inode,其中存储着常用的一些信息(所有者、创建时间、修改时间、文件权限、对应文件对象在系统中存储块的位置等等)
操作系统访问一个文件时分为三个步骤:

  • 通过文件名找到对应的 inode 编号

  • 通过 inode 编号访问对应文件对象的元信息

  • 根据元信息找到文件对应的扇区下对应的数据块,读取数据

🪜 当然了这是简化的流程,实际读取文件比这个复杂一些。还会涉及到内核调用和页缓存等其他操作。

从这个例子可以发现inode像是一种索引,并且会记录用来指向实际的存储数据块的地址。

🍺 这里是不是发现有点像指针了。

三、从Java和Go的角度看指针

1、众所周知Java语言是没有“指针”的。Java虽没有切实的指针类型,但是这个东西是切实存在的。比如Java的引用类型(除了八种基本类型外都是引用类型)实际在存储时都是把引用(堆上的地址)储存在栈,(对象的数据)储存在堆中。然后通过引用的指向来进行对象数据的读取。(🔥是不是inode指向数据块的思维)

例如:

class Dog {
String name;
int age;
}

class Demo {
public static void main(String[] args) {
Dog dog = new Dog(); // 这里实际上就是在栈中放进去一个dog(引用),然后在堆中开辟了一块空间用来供dog存东西。
dog.name = "库奇"; // 这时就可以拿着dog存的地址去堆里面给age和name赋值
dog.age = 1; // 其实整个可以理解成文件系统中 dog.mp4 ---> 实际的mp4数据 (不太恰当,能理解就行)

// 这个时候还可以测试一下Java中的值传递是什么逻辑
System.out.println("1. name:" + dog.name + " age:" + dog.age);
// 打印结果:1. name:库奇 age:1
change(dog);
System.out.println("2. name:" + dog.name + " age:" + dog.age);
// 打印结果:2. name:阿奇 age:2
// 程序成功运行,中途不会报错。
}

static void change(Dog dog) {
dog.age = 2;
dog.name = "阿奇";
dog = null; // 这一行在idea中你会发现有"The value null assigned to 'dog' is never used"提示
}
}

由此可见这里面的值传递并不是传递dog本身过去,而是把dog这个引用复制了一份传给change方法。然后change方法拿到引用去找到堆里面的数据进行修改(修改堆数据肯定会影响dog最后的输出),然后dog = null只是把克隆的这个dog引用地址给置空了,完全不影响原dog引用指向堆中。

2、博主也会Go语言,在Go语言中是有指针这个东西的。他是切实存在的类型。Go的世界中new出来的都是指针(Java中new出来的都是“引用”)。这里和Java最大的区别就是Go语言的函数是可以将指针作为参数传入,也可以将引用作为参数传入。看来起比Java更加灵活了。

例如:

package main

import "fmt"

type Dog struct {
name string
age int8
}

func main() {
dog := Dog{
name: "库奇",
age: 1,
}
fmt.Printf("地址:%p 1 name:%s age:%d \n", &dog, dog.name, dog.age)
// 输出:地址:0xc0000a4018 1 name:库奇 age:1
change1(dog)
fmt.Printf("地址:%p 1 name:%s age:%d \n", &dog, dog.name, dog.age)
// 输出:地址:0xc0000a4018 1 name:库奇 age:1
change2(&dog)
fmt.Printf("地址:%p 1 name:%s age:%d \n", &dog, dog.name, dog.age)
// 输出:地址:0xc0000a4018 1 name:路玛 age:3
}

func change1(dog Dog) {
fmt.Printf("change1中dog地址:%p \n", &dog)
// 输出:change1中dog地址:0xc0000a4030
dog.name = "阿奇"
dog.age = 2
}

func change2(dog *Dog) {
fmt.Printf("change2中参数指针的地址:%p \n", dog)
// 输出:change2中参数指针的地址:0xc0000a4018
fmt.Printf("change2中参数指针变量的指针地址:%p \n", &dog)
// 输出:change2中参数指针变量的指针地址:0xc0000ac020
dog.name = "路玛"
dog.age = 3
dog = nil // 此处把指针变量dog的值置空
}

由上面的示例可以发现change1中的参数(dog Dog)实际上就是main中dog的复制品。所以他的内存地址(0xc0000a4030)和main中dog的内存地址(0xc0000a4018)不一致。所以在change1中进行的所有修改都是针对dog(0xc0000a4030)的修改,与main方法的中dog变量没有任何关系。

而change2中的参数是指针类型,它相当于使用了一个指针参数(dog *Dog)来接收0xc0000a4018的值(⚠️ 这里的dog的值是0xc0000a4018,而dog自己又是一个变量所以他自己也有内存地址0xc0000ac020)。所以后续对name和age的操作实际上是针对0xc0000a4018指向的name和age进行操作,与它本身指针变量没有关系。指针变量只有一个属性那就是地址值(0xc0000a4018),这时dog = nil其实是修改了指针变量dog的值(0xc0000a4018)为nil,与main中的dog变量没有关系。所以最后调用change2后依旧输出的是地址:0xc0000a4018 1 name:路玛 age:3