前言
关于之前「Go语言的内存管理实现」这部分,本来接下来是要给大家继续讲解「Go语言堆内存、栈内存分配」的,以及这部分之前图都画完了。但是呢,写文章的时候,写着写着发现写不下去了,为什么?
我发现想要彻底理解「Go语言堆内存、栈内存分配」必须需要很清楚的理解:什么是堆内存和栈内存,看到这里肯定很多朋友笑了「堆内存/栈内存」那不简单么:
- 栈内存:栈内存的分配和释放都是由程序自己操作的,无需我们程序员关心。
- 堆内存:一种是编程语言要求由我们程序员分配、释放堆内存,不小心的话就会带来内存泄漏;另一种由于编程语言自己实现了垃圾回收器,堆内存的分配、释放也不需要由我们程序员关心了,垃圾回收器会自动回收堆内存。
确实,上面的说法没什么问题。但是呢,本着刨根问底的态度,我再来引申几个问题:
- 栈内存到底存在于哪?栈内存具体是被谁分配和释放的?栈内存分配和释放的具体时机是什么时候?
- 有了栈内存为什么还需要堆内存?堆内存到底存在于哪?为什么只有堆内存需要垃圾回收机制回收?
为了彻底帮助大家简单明了的吃透「堆内存、栈内存」,又不断引申出了新的问题:
我们写的代码、以及运行起来的程序到底是什么?
在解决这个问题的时候,却又引申出了新的问题:
刨根问底,计算机到底是怎么运行起来的?
最后这样一步步引申到了「计算机原理」和「计算机的发展史」,为了更好理解这些问题,于是这两个月我又读了三本书,具体如下:
- 《程序是怎么跑起来的》
- 《计算机是怎样跑起来的》
- 《计算机:一部历史》
我把读完这三本书学习到的知识点进行整理,以及结合自己的理解,抽丝拔茧帮助大家去理解:
- 我们写的代码到底是什么?
- 运行的程序到底是什么?
- 计算机到底是什么?
搞定了这些问题,我们再去学习「Go语言堆内存、栈内存」或者别的语言的「堆内存、栈内存」都会得心应手,容易很多。
正文
本篇文章是自己对这两个月学习内容的总结,同时也帮助大家对计算机发展历史有一个基本的认知,本文主要目录如下:
- 计算机启蒙于数学
- 计算机发展于电子学+数学
- 具有语义的编程语言
- 程序集合:操作系统
计算机启蒙于数学
理论计算机:图灵机
上个世纪伟大的数学家们发起了一个挑战大概意思是:“制造一台机器可以自动计算数学问题”。当时伟大的数学家艾伦·图灵提出了自己的理论,大概思路是通过输入之后机器可以自动计算并输出结果,这个理论就是大名鼎鼎的“图灵机”,艾伦·图灵就是理论计算机“图灵机”的创造者。
算术问题也是逻辑问题
不难理解,数学问题都可以转化为两类问题:
- 算术问题:四则运算等
- 逻辑问题:与、或、非、异或等
算术问题可以通过逻辑运算解决,所以所有数学问题都可以看作是逻辑问题。所以,只要找到可以自动判定真假的某种机器即可实现自动计算。
这里就有人疑惑🤔了,逻辑问题怎么解决算术问题的,想要理解这个问题我们先回到小学加法运算,比如16+36的计算过程:
16
36
+
------
2
4 1(进位)
------
52
这个运算过程我们只需要重点关注以下几点:
- 从左到右按位进行10以内的加法运算(算术问题)
- 保存当前位的计算结果(存储)
- 当前位计算结果是否需要进位(逻辑问题是否需要进位)
目前进位的问题已经可以转化为逻辑问题了。以上是十进制的加法运算过程,我们换成更简单二进制加法运算再来看看,如下:
最简单的二进制
二进制相对于十进制更加简单,只有0和1两个状态,简单回顾下二进制的加法运算过程:
为了简化理解这里以十进制 3 + 6 的二进制运算过程为例:
二进制分别为:
0011
0110
A:加数
B:被加数
C:进位,当前位运算后的进位结果,有进位则进位值为1,无进位则进位值为0
D:结果,当前位运算结果
C1:进位,上一位运算后的进位
二进制计算过程:
<------从右到左最低位开始逐位运算------
A 0 0 1 1
^ 按位异或运算
B 0 1 1 0
---------------------计算第1位
D 1
C 0
C1 0
^
A 1
^
B 1
---------------------计算第2位
D 0
C 1
C1 1
^
A 0
^
B 1
---------------------计算第3位
D 0
C 1
C1 1
^
A 0
^
B 0
---------------------计算第4位
D 1 0 0 1
- A:加数
- B:被加数
- C:进位,当前位运算后的进位结果,有进位则进位值为1,无进位则进位值为0
- D:结果,当前位运算结果
- C1:进位,上一位运算后的进位
A | B | C1 | D | C |
---|---|---|---|---|
- | - | - | A异或B异或C1 | (A 与 B) 或者 (C1 与 (A 或 B)) |
1 | 0 | 0 | 1 | 0 |
1 | 1 | 0 | 0 | 1 |
0 | 1 | 1 | 0 | 1 |
0 | 0 | 0 | 1 | 0 |
- 结果D表达式: D = A 异或 B 异或 C1
- 进位C表达式: C = (A 与 B) 或者 (C1 与 (A 或 B))
以上过程把十进制 3 + 6 的算术运算完全转化为了逻辑问题,所以只要找到可以自动判定真假的某种机器即可实现自动计算。
计算机发展于电子学和数学
上个世纪电子学开始广泛发展,晶体管的诞生为理论上的「自动的机器」指明了新的方向🧭。晶体管有两个状态,分别是:
- 导通:即可以代表二进制的1
- 截止:即可以代表二进制的0
接着,数学家&电子学家们用多个晶体管构成门电路:
- 与门
- 非门
- 或门
- 异或门
这样就实现了可以「自动判定逻辑问题的真假的设备」,同时数学家和电子学家们又通过多个门电路实现了半加器、全加器、全减器、乘法器等实现算术运算:
- 半加器
- 全加器
- 全减器
- 乘法器
这样就诞生了世界上第一台真正意义的计算机。
具有语义的编程语言
上面诞生了硬件,也就是真正意义的计算机。问题是如何编写程序?我们的程序其实就是门电路中的晶体管不断的运行导通1和截止0两个状态之间,对应的文本代码其实就是数字0和1,所以早期的代码就是直接编写0和1的代码。但是对于人类阅读友好的永远是具备描述性的具备可读性的文本,于是诞生了更适合人们阅读和编写的汇编代码。
汇编代码
人们把中央处理器CPU可以运行的一系列指令集合分别命名了具备可读性的文本,这样就诞生了由助记符和操作数等组成的汇编语言。通常来看CPU一般具备四类指令,分别为:
- 数据传输指令
- 运算指令
- jmp指令
- call/return指令
简单来看就这些类别的指令,分别给它们用更适合人类阅读文本替代0和1,比如MOV指令代表传输数据、jmp指令代表跳转到代码任意位置。
同时人们发明了编译器自动把汇编代码转换为0和1组成的机器代码。
子函数和函数库
同时人们发现编写程序过程中,发现经常会出现重复性的逻辑编写,比如算术平方根。为了提高效率和复用,就诞生了子函数和函数库的概念。
编程语言和集成开发环境
随着计算机技术和编程技术的快速发展,逐步诞生了更高效的编程语言、以及集成开发环境等等。
程序集合:操作系统
先来看看计算机的组成:
- 输入设备
- 输出设备
- 中央处理器CPU:运行指令
- 存储器:内存、磁盘等
最早工程师编写的程序是可以直接操作这些硬件设备的:
存在问题:每个编写程序的工程师都要实现对这些硬件的操作,存在大量重复的工作,以及安全性等等问题。
于是对硬件设备的操作进行统一的封装,比如对输入/输出设备的操作,对磁盘的操作等等 这样就形成了一系列统一的API以及应用程序,提升了开发效率也保证了安全等等。
这就是操作系统:封装了一系列对计算机硬件设备操作的API和应用程序的集合。
结语
计算机简易的关键发展点:
编程语言的关键发展点:
本文从一些关键点回顾了计算机的发展史,为后面理解程序的运行打好基础。下篇文章我们就来看看:
我们写的代码到底是什么?