这本科普书感觉很棒
普通的程序员主要关注寄存器极客
程序是把寄存器作为对象来描述的
汇编
汇编语言采用**助记符(memonic)**来编写程序,每 一个原本是电气信号的机器语言指令都会有一个与其相应的助记符, 助记符通常为指令功能的英语单词的简写。例如,mov 和 add 分别是 数据的存储(move)和相加(addition)的简写
通常我们将汇编语言编写的程序转化成机器语言的过程称为汇编;反之,机器语言程序转化成汇编语言程序的过程则称为反汇编
把汇编语言转化成机器语言的程序称为汇编器(assembler)。有时汇编语言 也称为汇编
编译是指将使用高级编程语言编写的程序转换为机器语言的过程,其中, 用于转换的程序被称为编译器(compiler)。
寄存器主要种类
启发挺大的
程序的流程分为顺序执行、条件分支和循环三种。
条件分支和循环中使用的跳转指令,会参照当前执行的运算结果 来判断是否跳转。
标志寄存器
CPU执行比较的机制
哪怕是高级语言编写的程序, 函数 调用处理也是通过把程序计数器的值设定成函数的存储地址来实现的。不过,这和条件分支、循环的机制有所不同,因为单纯的跳转指令无法实现函数的调用。函数的调用需要在完成函数内部的处理后,处理流程再返回到函数调用点(函数调用指令的下一个地址)。因此,如果只是跳转到函数的入口地址,处理流程就不知道应该返回至哪里了
上图是给变量 a 和 b 分别代入 123 和 456 后,将其赋值给参数 (parameter)来调用 MyFunc 函数的 C 语言程序。
图中的地址是将 C语言编译成机器语言后运行时的地址。由于 1 行 C 语言程序在编译后通常会变成多行的机器语言,所以图中的地址是离散的
call和return指令
解决上面这个问题,函数调用使用的是 call 指令,而不是跳转指令。 在将函数的入口地址设定到程序计数器之前,call 指令会把调用函数后要执行的指令地址存储在名为栈 A的主存内。
函数处理完毕后,再通过 函数的出口来执行 return 命令。
return 命令的功能是把保存在栈中的地址设定到程序计数器中。如图 1-7 所示,MyFunc 函数被调用之前, 0154 地址保存在栈中。MyFunc 函数的处理完毕后,栈中的 0154 地址 就会被读取出来,然后再被设定到程序计数器中(图 1-8)
在编译高级编程语言的程序后,函数调用的处理会转换成 call 指 令,函数结束的处理则会转换成 return 指令。这样一来,程序的运行也就变得非常流畅
基址寄存器和变址寄存器
居然和数组联系起来了好耶
CPU和内存是 IC 的一种
IC 的所有引脚,只有直流电压 低电平 或 高电平 两个状态。也就是说,IC 的一个引脚,只能表示两个状态。
IC 的这个特性,决定了计算机的信息数据只能用二进制数来处理。 由于 1 位(一个引脚)只能表示两个状态,所以二进制的计数方式就变 成了 0、1、10、11、100…这种形式。
计算机处理信息的最小单位—— 位,就相当于二进制中的一位。位的英文 bit 是二进制数位 (binary digit)的缩写
8 位二进制数被称为一个字节 。
字节是最基本的信息计量单位。
位是最小单位, 字节是基本单位。
内存和磁盘都使用字节单位来存储和读写数据
基数:数值的表现方法,进位计数制中各数位上可能有的数值的个数。十进制数 的基数是 10,二进制数的基数是 2
位权:“○○的 ×× 次幂” ,
移位运算:将二进制数值的各数位进行左右移位(shift = 移位)的运算。
例
不过,移位运算也可以通过数位移动来代替乘法运算和除法运算。 例如,将 00100111 左移两位的结果是 10011100,左移两位后数值变成 了原来的 4 倍。用十进制数表示的话,数值从 39(00100111)变成了 156(10011100),也正好是 4 倍(39×4 = 156)
2.3中之所以没有介绍有关右移的内容,是因为用来填充右移后空出来的高位的数值,有 0 和 1 两种形式。要想区分什么时候补 0 什么 时候补 1,只要掌握了用二进制数表示负数的方法即可
二进制数中表示负数值时,一般会把最高位作为符号来使用,因 此我们把这个最高位称为符号位。符号位是 0 时表示正数 ,符号位是 1 时表示负数。
计算机在做减法运算时,实际上内部是在做加法运算。用加法 运算来实现减法运算
这本书感觉这节讲的并不是很清楚…
逻辑右移
算术右移
将二进制数作为带符号的数值进行运算时,移位后要在最高位填充移位前符号位的值(0或1)。这就称为算术右移。
只有在右移时才必须区分逻辑位移和算术位移。左移时,无论是 图形模式(逻辑左移)还是相乘运算(算术左移),都只需在空出来的 低位补 0 即可
符号扩充
我们不妨这样考虑,将二进制数表示的信息作为四则运算 的数值来处理就是 算术。而像图形模式那样,将数值处理为单纯的 0 和 1 的罗列就是 逻辑
例:白色为1,黑色为0
这里挺有意思
这个网格让我想到了计算机图形学的实验
例
程序没错,计算机也没有发生故障,当然,C语言也没有什么问题。可为什么会出现这样的结果呢?这时,如果考虑一下计算机处理小数的机制,就讲得通了。那么,计算机内部是如何处理小数的呢?
二进制数小数点前面部分的位权,第 1 位是 2 的 0 次幂、第 2 位 是 2 的 1 次幂……以此类推。小数点后面部分的位权,第 1 位是 2 的-1 次幂、第 2 位是 2 的-2 次幂,以此类推
例
原因:有一些十进制数的小数无法转换成二进制数
例如,十进制数 0.1,就无法用二进制数正确表示,小数 点后面即使有几百位也无法表示
图3-2中,小数点后4位用二进制数表示时的数值范围为0.0000一0.1111。因此,这里只能表示0.5、0.25、0.125、0.0625这四个二进制数小数点后面的位权组合而成(相加总和)的小数。将这些数值组合后能够表示的数值,即为表3-1中所示的无序的十进制数。
表3-1中,十进制数0的下一位是0.0625。因此,这中间的小数,就无法用小数点后4位数的二进制数来表示。同样,0.0625的下一位数一下子变成了0.125。这时,如果增加二进制数小数点后面的位数,与其相对应的十进制数的个数也会增加,但不管增加多少位,2的-○○次幂怎么相加都无法得到0.1这个结果。实际上,十进制数0.1转换成二进制后,会变成0.00011001100… ( 1100循环)这样的循环小数。这和无法用十进制数来表示1/3是一样的道理。1/3就是0.3333…,同样是循环小数。
至此,大家应该明白了为什么用代码清单3-1的程序无法得到正确结果了吧。因为无法正确表示的数值,最后都变成了近似值。计算机这个功能有限的机器设备,是无法处理无限循环的小数的。因此,在遇到循环小数时,计算机就会根据变量数据类型所对应的长度将数值从中间截断或者四舍五入。我们知道,将0.3333…这样的循环小数从中间截断会变成0.333333,这时它的3倍是无法得出1的(结果是0.999999 )、计算机运算出错的原因也是同样的道理。
https://akaedu.github.io/book/ch14s04.html
双精度浮点数类型用 64 位、单精度浮点数类型用 32 位来表示全体小数
在 C 语言中,双精度浮点数类型和单精 度浮点数类型分别用 double 和 float 来表示。不过,这些数据类型都采 用浮点数 来表示小数。
浮点数是指用符号、尾数、基数和指数这四部分来表示的小数
尾数部分和指数部分并不只 是单单存储着用整数表示的二进制数。尾数部分用的是“将小数点前面 的值固定为 1 的正则表达式”,而指数部分用的则是“EXCESS 系统表现”
这部分没完全看懂,以后回头再来看
按照特定的规则来表示数据的形式即为正则表达式。除小数之外,字符串 以及数据库等,也都有各自的正则表达式
尾数部分使用正则表达式
指数部分使用的EXCESS系统
EXCESS系统 百度百科
读到这里,有人额角冒汗吗?上述内容不是仅仅读一遍就能马上理解的,最好能够在实际的程序中加以确认。因此,我们准备了一个试验用的程序,如代码清单3-2所示。接下来,就让我们一起看一下如何用单精度浮点数来表示十进制数0.75吧。
该程序执行后,十进制数 0.75 用单精度浮点数来表示就变成了 0-01111110-10000000000000000000000(图 3-7)。加入破折号(-)是为 了区分符号部分、指数部分、尾数部分。这里,符号部分为 0,指数部 分为 01111110,尾数部分为 10000000000000000000000。因为 0.75 是 正数,所以符号位是 0。指数部分的 01111110 是十进制数 126,用 EXCESS 系统表现就是- 1(126- 127 =- 1)。根据正则表达式的规则, 小数点前面的第 1 位是 1,因此尾数部分 10000000000000000000000 实 际上表示的是 1.10000000000000000000000 这个二进制数。将尾数部分 的二进制数转换成十进制数,结果就是(1 × 2 的 0 次幂)+(1 × 2 的-1次幂)= 1.5。因此,0-01111110-10000000000000000000000 这个单精度浮点数,表示的就是“+ 1.5 × 2 的-1 次幂”。2 的-1 次幂是 0.5,+ 1.5 × 0.5 = + 0.75。正好吻合,结果正确
那个指数应该就是直观感觉上的移动位数再使用EXCESS系统处理一下
计算机计算出错的原因之一是,采用浮点数来处理小数(另外,也有因“位溢出”而造成计算错误的情况)。作为程序的数据类型,不管是使用单精度浮点数还是双精度浮点数,都存在计算出错的可能性。接下来将介绍两种避免该问题的方法。
首先是回避策略,即无视这些错误。根据程序目的的不同,有时一些微小的偏差并不会造成什么问题。例如,假设使用计算机设计工业制品。将100个长0.1毫米的零件连接起来后,其长度并非一定要是10毫米,10.000002毫米也没有任何问题。一般来讲,在科学技术计算领域,计算机的计算结果只要能得到近似值就足够了。那些微小的误差完全可以忽略掉。
另一个策略是把小数转换成整数来计算。计算机在进行小数计算时可能会出错,但进行整数计算(只要不超过可处理的数值范围)时一定不会出现问题。因此,进行小数的计算时可以暂时使用整数,然后再把计算结果用小数表示出来即可。例如,本章一开头讲过的将0.1相加100次这一计算,就可以转换为将0.1扩大10倍后再将1相加100次的计算,最后把结果除以10就可以了(代码清单3-3)。
在以位为单位表示数据时,使用二进制数很方便,但如果位数太多,看起来就比较麻烦。因此,在实际程序中,也经常会用十六进制数来代替二进制数。在C语言程序中,只需在数值的开头加上0x (0和x )就可以表示十六进制数,
二进制数的4位,正好相当于十六进制数的1位。例如,32位二进制数00111101110011001100110011001101用十六进制数来表示的话,就是3DCCCCCD这个8位数。由此可见,通过使用十六进制数,二进制数的位数能够缩短至原来的1/4。位数变少之后,看起来也就更清晰了(图3-9)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oN05HKJD-1669650779520)(https://cdn.jsdelivr.net/gh/xin007-kong/picture_new/img/20221128233550.png)]
用十六进制数来表示二进制小数时,小数点后的二进制数的4位也同样相当于十六进制数的1位。不够4位时用О填补二进制数的低位即可。例如,1011.011的低位补0后为1011.0110,这时就可以表示为十六进制数B.6(图 3-10)。十六进制数的小数点后第1位的位权是即 1/16=0.0625
内存实际上是一种名为内存 IC 的电子元件。虽然内存 IC 包 括 DRAM、SRAM、ROMA 等多种形式,但从外部来看,其基本机制都 是一样的。内存 IC 中有电源、地址信号、数据信号、控制信号等用于 输入输出的大量引脚(IC 的引脚),通过为其指定地址(address),来 进行数据的读写
读写数据图示
C语言程序案例
下面我们来看一个具体的示例。如代码清单4-1所示,这是一个往a、b、c这3个变量中写入数据123的C语言程序。这3个变量表示的是内存的特定区域。通过使用变量,即便不指定物理地址,也可以在程序中对内存进行读写。这是因为,在程序运行时,Windows等操作系统会自动决定变量的物理地址。
指针也是一种变量,它所表示的不是数据的值,而是存储着数据的内存的地址。
通过使用指针,就可以对任意指定地址的数据进行读写。
虽然前面所提到的假想内存 IC 中仅有 10 位地址信号,但在 Windows 计算机上使用的程序通常都是 32 位(4 字节)的内存地址。 这种情况下,指针变量的长度也是 32 位。
woc,突然知道为啥指针是4字节了!!!
举例
从内存的角度读懂C指针 - 开发者共读的文章 - 知乎 https://zhuanlan.zhihu.com/p/249330470 这篇知乎文章写的有些错误,不过总体还行
数组是指多个同样数据类型的数据在内存中连续排列的形式。
作为数组元素的各个数据会通过连续的编号被区分开来,这个编号称 为索引(index)。指定索引后,就可以对该索引所对应地址的内存进行 读写操作 。而索引和内存地址的变换工作则是由编译器自动实现的。
例
数组的定义中所指定的数据类型,也表示一次能够读写的内存大小。char 类型的数组以 1 个字节为单位对内存进行读写,而 short 类型 和 long 类型的数组则分别以 2 个字节、4 个字节为单位对内存进行读写。
之所以说数组是内存的使用方法的基础,是因为数组和内存的物理构造是一样的。特别是 1 字节类型的数组,它和内存的物理构造完全一致。不过,如果只能逐个字节地来读写,程序就会变得比较麻烦, 因而可以指定任意数据类型来定义数组。
栈 和队列,都可以不通过指定地址和索引来对数组的元素进行读写。需要临时保存计算过程中的数据、连接在计算机上的设备或者输入 输出的数据时,都可以通过这些方法来使用内存。如果每次保存临时数据都需指定地址和索引,程序就会变得比较麻烦,因此要加以改进。
栈和队列的区别在于数据出入的顺序是不同的。在对内存数据进 行读写时,栈用的是 LIFO(Last Input First Out,后入先出)方式,而 队列用的则是 FIFO(First Input First Out,先入先出)方式。
如果我们 在内存中预留出栈和队列所需要的空间,并确定好写入和读出的顺序, 就不用再指定地址和索引了。
如果要在程序中实现栈和队列,就需要以适当的元素数来定义一个用来存储数据的数组,以及对该数组进行读写的函数对。当然,在 这些函数的内部,对数组的读写会涉及索引的管理,但从使用函数的 角度来说,就没有必要考虑数组及索引了
暂且把往栈中写入数据的函数命名为 Push,把从栈中 读出数据的函数命名为 Pop,
队列一般以环状缓冲区实现
链表和二叉查找树,都是不用考虑索引的顺序就可 以对数组元素进行读写的方式。通过使用链表,可以更加高效地对数 组数据(元素)进行追加和删除处理。而通过使用二叉查找树,则可以 更加高效地对数组数据进行检索
在数组的各个元素中,除了数据的值之外,通过为其附带上下一 个元素的索引,即可实现链表。数据的值和下一个元素的索引组合在一起,就构成了数组的一个元素。这样,数组元素相连就构成了念珠似的链表。由于链表末尾的元素没有后续的数据,因此就需要用别的值(在这里是-1)来填充
在需要追加或删除数据的情况下,使用链表是很高效的。首先,让我们来看一下删除的情况。在图4-10表示的链表中,假设要删除从起始位置开始的第3个元素。此时,我们只需要把第2个元素的“下一个元素:2”变成“下一个元素:3”即可。由于数组的元素通常是按照索引顺序来引用的,因此当我们需要引用构成链表的数组的某一个元素时,通过该元素的索引信息就可以找到下一个元素。当第2个元素的下一个元素变成第4个元素后,那么第3个元素就被删除了。虽然第3个元素在物理内存上还残留着,但在逻辑上则确实被删除了(图4-11 )。
接下来就让我们来看一下如何往链表中追加数据。假设要在图4-10的链表的第5位前追加一个新数据。此时,我们只需要在刚才消除的第3个元素的位置中保存新的数据,并将第4个元素的“下一个元素:5”变更成“下一个元素:2”,以使新追加的元素的索引信息变成“下一个元素:5”即可。虽然新追加的元素在物理上是第3个,但从逻辑上看来则是第5个(图4-12)。
为了实现二叉查找树,怎么处理比较好呢?其实数组的每个元素中只要有数据的值和两个索引信息就可以了。图4-16向我们展示了如何用数组来实现图4-14中的二叉查找树。二叉查找树是由链表构造发展而来的表现形式,因此在追加或删除元素方面也同样是有效的。
使用二叉查找树的便利之处在于可以使数据的搜索等更有效率。在使用一般的数组时,必须从数组的开头按照索引顺序来查找目标数据。而使用二叉查找树时,当目标数据比现在读出来的数据小时就可以转到左侧,反之目标数据较大时即可转到链表的右侧,这样就加快了找到目标数据的速度。
之前学数据结构的时候没有思考到这一层…
数组中。那么,如果接下来的值比先前保存的数值大的话,就要将其放到右边,反之如果小的话就放在左边。但实际的内存并不会分成两个方向,这是在程序逻辑上实现的(4-15)