我学习 C++ 遇到的第一个难以理解的点就是指针,一级指针多级指针搞得晕头转向,为了理解指针,早些年看了很多文章也看过许多视频。当时我都是这么类比的:变量就是我的名字,指针就是我的住址,引用就是我的别名,二级指针是住址的住址,三级指针那就是住址的住址的住址,再往后以此往下类推。
1. 类比法理解
int main() {
// 定义变量
int number = 1;
// 获取指针
int* p_number = &number;
// 通过指针操作变量赋值
*p_number = 2;
// cout << "number = " << number << endl;
return 0;
}
以上是一个最简单的代码,number 就是变量的名字,p_number 就是通过变量拿到了住址,*p_number 通过住址找到变量,可以间接的操作变量赋值 。这样理解几乎能解决所有开发中遇到的问题,除非特别复杂奇怪的场景。那后面 new 对象出来的指针呢?其实本质上也都是一样的。
2. 从计算机电路来理解指针
所谓类比,只是方便大家记忆,其实就是说完全不是这么一回事。在计算机看来,变量、指针、住址、别名等等这些,都是不存在的,唯一存在的就是内存高低电压(地址)。接下来,我们试着从计算机电路的角度来理解指针。
这图是我们上面的代码编译出来的结果,红色标记的是 cpu 最终要执行的机器码指令,这里是以 16 进制来呈现的,最终编译出来是 101101… 这样。蓝色标记(希望我没色盲)的是汇编指令,这个是编译的中间产物代码,为了方便我们理解所以我把汇编指令也放出来了。汇编和机器码指令这些不在本文详细解释,不是我们本文的重点,后面我会陆续写一些文章。写文章实在是太耗时间了,一篇文章几乎花掉我一周的业余时间,但视频我可能 10 分钟就能讲清楚。这里我简单用文字解释,大家感兴趣可以去这个网站实践代码:compiler explorer
int number = 1;
// 对应如下
mov DWORD PTR [rbp-0xc],0x1
int* p_number = &number;
// 对应如下
lea rax,[rbp-0xc]
mov QWORD PTR [rbp-0x8],rax
*p_number = 2;
// 对应如下
mov rax,QWORD PTR [rbp-0x8]
mov DWORD PTR [rax],0x2
- mov DWORD PTR [rbp-0xc],0x1:cpu 读到这条指令将向内存地址 [rbp-0xc] 写入 0x1
- lea rax,[rbp-0xc]:cpu 读到这条指令将 [rbp-0xc] 的地址值赋值给 rax 寄存器
- mov QWORD PTR [rbp-0x8],rax:cpu 读到这条指令将 rax 寄存器的值写入 [rbp-0x8] 内存地址
- mov rax,QWORD PTR [rbp-0x8]:cpu 读到这条指令将 [rbp-0x8] 的值赋值给寄存器 rax
- mov DWORD PTR [rax],0x2:cpu 读到这条指令将向 rax 内存地址上存的值当作目标地址,写入 0x2,注意这里的 rax 用 [ ] 围起来了
2.1 cup 操作内存地址
- cpu 通过地址总线可以选择操作内存条上的某个位置,早期 cpu 只有 20 根地址总线内存只有 1M,现在一般都有 32 根地址线那么就可以达到 4GB。这个就是我们本文所说的内存地址
- cpu 通过读写操作线可以选择是读入数据还是写入数据
- cpu 通过数据总线把数据写入内存条,也可以从内存中把数据读入寄存器,32 根线代表我们一次能读取 32 位数据,64 根代表我们一次能读取 64 位数据,也就是我们常说的 32 位计算机与 64 位计算机
现在我们可以来尝试着翻译一下 cup 是如何操作内存地址的了,注意此处省略 cpu 读取指令的操作等等细节,对应代码如下:
int* p_number = &number;
翻译成汇编代码如下:
lea rax,[rbp-0xc]
mov QWORD PTR [rbp-0x8],rax
翻译成 cpu 机器指令如下:
01001000100011010100100111110100
01001000100010010100100111111000
假设我们现在 rbp 寄存器的值存的是 0x0f0f0f0d(32位计算机)第一条指令不需要操作内存,把寄存器 rbp 的值减掉 12 存到 rax 寄存器上,执行完第一条指令后,寄存器 rax = rbp - 12 = 0x0f0f0f01。第二条指令需要操作内存,把 rax(0x0f0f0f01) 的值写入到 0x0f0f0f0f5 的内存地址上。将上图的读写操作线设置为写,往读写操作线上传输高低电压来控制读写。往地址总线上传输高低电压来选择地址,32 根地址总线上传输高低电压为 00001111000011110000111100001001(0x0f0f0f0f5)往 32 根数据线上传输高低电压为 00001111000011110000111100000001(0x0f0f0f01)执行完当前指令后,以此类推再去读取下一条指令,继续执行代码。
2.2 cup 内部构造
- 寄存器:像上面的 rax、rbp 代表的都是 cpu 内部的寄存器,这些电路用来临时存放数据,有记忆功能
- 计数器:从哪里开始取指令,取完第一条指令执行完就要去取下一条指令,有计数功能
- 计算器:有些指令需要做加减乘除,有计算功能
- 控制器:读取指令译码,操作执行指令,控制内存、IO 等,有控制功能
无论多复杂的 CPU 内部都是由电子元器件(主要是晶体管)组成的数字电路,寄存器电路最简单,控制器电路最复杂。这些不在本文的范围内,我有打算后面录制一些科普视频,从电子元器件 -> 数字电路(门电路) -> CPU 内部的详细电路。关于 CPU 芯片卡脖子的制造工艺和历史,我推荐我们 WXG 的微信读书 App 里面的《芯片战争》
3. 最后杂谈
高级语言 -> 汇编语言 -> cpu工作原理 -> 数字电路,再加上数据结构算法、编译原理、操作系统和虚拟机。这个是我的一个学习路径,仅供大家参考。知识浩瀚无限,人的精力和时间却有限,仅仅是一个 linux 操作系统就有上千万行代码,所以我也是一无所知。原则上我们需要尽可能熟悉我们工作的下一层原理,比如以前我做 Android 的时候会花尽可能多的精力去阅读 Framework 的源码,所以之前对于 Android 的分享到 Framework 就打止了。我现在做 iOS 和 C++ 需要了解的就更多了一些。
现代计算机很复杂,我上面画的图非常简单,比如 CPU 内部现在一般都是多核、指令执行有指令流水线、cpu 内部还有多级的快速缓存。我们移动端开发的应用都是运行在操作系统上的,对于我们应用层来说都是虚拟地址,操作系统加上硬件一起配合才会转成真实的物理地址。早期的计算机并没有这么复杂,更有利于大家学习,所以推荐大家先去了解本质,从简单上手再到复杂。
有些语言看似很复杂,但只要是跑在计算机上,那么本质都是一样的。汇编、c/c++、OC、swift 等等只是语法可能有点不一样,但是最终本质大家都是一样的。有些语言依赖虚拟机比如 Java 语言,但 Java/Android 虚拟机是 C/C++ 参杂汇编写的。有些语言依赖解释器比如 python,解释器也 是 C/C++ 参杂汇编写的。学习一门语言的语法其实不用花太多时间,我记得之前从 Android 转到 iOS 开发也就用了一周的时间,关键还是我们对于底层原理的熟悉。
视频链接: pan.baidu.com/s/17u7btNnxOaWYoZ1E1FxBTA
视频密码: nt29