如果我们要将引用类型聊明白,我们需要从指针类型开始。指针类型的变量存储的值,其实是一个内存地址,这块内存地址可能在栈上,也可能在堆上。而且指针还支持加减运算,可谓非常灵活。比如下面这个简单的例子
#include <stdio.h>
void swap(int *a,int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int main() {
int a = 9;
int b = 1;
swap(&a,&b);
printf("a: %d b: %d",a,b);
}
swap:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-24], rdi ; 将a的内存地址放入[rbp-24]这块内存中
mov QWORD PTR [rbp-32], rsi ; 将b的内存地址放入[rbp-32]这块内存中
mov rax, QWORD PTR [rbp-24] ; 将a的内存地址放入rax寄存器中
mov eax, DWORD PTR [rax] ; 读取a的内存地址对应的内存数据到eax寄存器中
mov DWORD PTR [rbp-4], eax ; 将eax的值即a对应的数据(9) 放入到[rbp-4]这块内存中
mov rax, QWORD PTR [rbp-32]
mov edx, DWORD PTR [rax]
mov rax, QWORD PTR [rbp-24]
mov DWORD PTR [rax], edx
mov rax, QWORD PTR [rbp-32]
mov edx, DWORD PTR [rbp-4]
mov DWORD PTR [rax], edx
nop
pop rbp
ret
.LC0:
.string "a: %d b: %d"
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 9
mov DWORD PTR [rbp-8], 1
lea rdx, [rbp-8] ; 获取变量b的内存地址 &b
lea rax, [rbp-4] ; 获取变量a的内存地址 &a
mov rsi, rdx
mov rdi, rax
call swap
mov edx, DWORD PTR [rbp-8]
mov eax, DWORD PTR [rbp-4]
mov esi, eax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov eax, 0
leave
ret
通过汇编代码我们可以发现,所谓的指针类型无非对应两条指令,一条为 lea
获取对应的内存地址,一条为 [memory_address]
获取内存地址对应的值。具体可以看我在代码中对应的注释。在这里,变量a与变量b都在main函数的栈帧中,属于栈内存。
通过上面的例子,我们可以发现我们获取一个变量的内存地址非常的容易,利用内存地址获取对应的数据也非常容易。但是引用类型就没有这么灵活,第一,我们不能随意的获取某个变量的内存地址,第二,也不存在通过某种运算符可以从内存地址提取出对应的值。而且引用类型存储的内存地址,都是堆内存。比如下面这段代码
Object obj = new Object();
这段代码的含义是,在堆内存中创建Object类型的对象,并将内存首地址赋值给obj
这个变量。当我们使用 obj.wait()
时,代表着,通过obj找到真实的对象,然后再通过对象去寻找对应的wait
方法
引用类型失去了随意获取内存地址的能力,这让他丧失了一定的灵活性,但是却更加的安全。
这里我们发现,无论是什么类型在CPU层面都消失了,那么类型究竟在哪里发挥了作用呢?主要有两个方面,第一,在我们编写代码的时候,类型能够让我们规避很多由于粗心导致的问题。第二,编译器在将高级编程语言转换为机器码的时候,提供了一些信息,比如操作内存的大小,是一个字节还是4个字节。以及对一些数据的类型做校验,如果不满足数据不满足这个类型的格式,那么就会报错,编译失败。
以上,就是我对指针以及引用的一些思考,希望对你有帮助。