第一章:对程序员来说CPU是什么
问题:
1:程序是什么
指示计算机每一步动作的指令(行事的先后顺序)
2:程序是由什么组成的
指令和数据 (比如pringf(“你好”),printf是指令,"你好"是数据)
3:什么是机器语言
cpu可以直接识别并使用的语言(C,Java最后都会转成机器语言)
4:正在运行的程序存储在什么位置
内存(硬盘和磁盘等媒介上保存的程序要复制到内存中才能运行)
5: 什么是内存地址
内存中用来表示命令和数据存储位置的数值
内存中保存命令和数据的场所,通过地址来标记和指定,地址由整数值表示
6:计算机的构成元件中,负责程序解释和运行的是哪个
CPU
根据程序指令进行数据运算,并控制整个计算机的设备称作CPU,比如奔腾(Pentium)就是CPU的一种
本章重点
CPU(Central Processing Unit),中央处理器,相当于计算机的大脑,内部由数百万个晶体管组成,需要弄清楚cpu是如何运行的。特别要弄清楚负责保存指令和数据的寄存器的机制
加载到内存中的机器语言程序,由CPU进行解析和运行
1.1 CPU的内部结构解析
程序运行的一般流程
cpu内部由寄存器、控制器、运算器和时钟四个部分构成,各部分之间由电流信号相互连通
- 寄存器:暂存指令,数据(可看成内存的一种),一个cpu内部大概有20-100个寄存器
- 控制器:负责把内存上的指令,数据等读入寄存器,并根据指令的执行结果控制整个计算机
- 运算器:负责运算从内存读入寄存器的数据
- 时钟:负责发出CPU开始计时的时钟信号,不过,有些计算器的时钟位于计算机的外部(Clock puzzle,时钟信号的频率越高,计算机的运行速度越快,Pentium 2Ghz表示时钟信号的频率为2*10亿次每秒)
内存:
通常指计算机的主存储器,简称主存,通过控制芯片与CPU相连,主要负责为存储指令和数据,主存由可读写的元素构成,每个字节(1字节等于8位)都带有一个地址编号,CPU通过该地址读取主存中的指令和数据,也可以写入数据,主存中存储的指令和数据会随着计算机的关机而自动清除,主存通常使用DRAM(Dynamic Random Access Memory)动态随机存取存储器
程序启动后,根据时钟信号,控制器会从内存中读取指令和数据,通过读这些指令加以解释和运行,运算器对数据进行运算,控制器根据该运算结果来控制计算机
1.2CPU 是寄存器的集合体
汇编语言采用助记符编写程序,每一个机器语言指令都有相应的助记符
汇编:将汇编语言编写的程序转化成机器语言
反汇编: 机器语言程序转化为汇编语言程序
使用高级编程语言(c/c++/java/basic/pascal/fortran/cobol)编写的程序,经过编译转化成机器语言后才能运行
机器语言和汇编语言称为低级编程语言
机器语言级别的程序是通过寄存器处理的
也就是说,使用高级语言编写的程序会在编译后转化成机器语言,然后再通过CPU内部的寄存器来处理
编译器:用于把高级语言转化成机器语言的程序
从下表可以看出,寄存器大致划分为8类,寄存器中存储的内容既可以是数据也可以是指令,数据分为用于运算的数值和表示内存地址的数值两种
程序计数器,累加寄存器,标志寄存器,指令寄存器和栈寄存器都只有一个,其他寄存器一般有多个
1.3 决定程序流程的程序计数器
地址0100程序运行的开始位置,windows等操作系统把程序从硬盘复制到内存后,会将程序计数器设定为0100,然后CPU每执行一个指令,程序计数器的值就会自动加1
1.4条件分支与循环机制
顺序执行:按照地址内容顺序执行指令
条件分支:根据条件执行任意地址的指令
循环:重复执行同一地址的指令
若程序中存在条件分支和循环,机器语言的指令就可以将程序计数器的值设定任意的地址,而不是+1,程序可以跳转到任意地址
溢出:运算的结果超出了寄存器的长度范围
程序的比较指令其实是CPU内部做减法运算
1位就是1个位数的二进制数,表示0或1的数值,32位CPU指的就是利用32位的二进制数来表示数据及地址的数值
1.5 函数的调用机制
很多高级编程语言都采用类似于y=f(x)这样的数学函数的语法来记述编写处理。我们知道,该数学函数的意思是将x这个值通过f处理后得到数值y。如果套用函数的语法,x就是参数,y就是返回值,执行函数的功能就是函数调用。
函数调用的处理是通过把程序计数器的值设定为函数的存储地址来实现,单纯的跳转指令无法实现函数的调用。函数的调用需要在完成函数内部的处理后,处理流程再返回到函数调用点(函数调用指令的下一个地址)。因此,如果只是跳转到函数的入口地址,处理流程就不知道应该返回至哪里了。
图中的地址是将C语言编译成机器语言后运行时的地址。由于1行C语言程序在编译后通常会变成多行的机器语言,所以图中的地址是离散的。
函数调用使用call指令,而不是跳转指令,因为跳转指令不知道函数执行完成,程序计数器应该跳转到哪一个地址
在将函数的入口地址设定到程序计数器之前,call指令会把调用函数后要执行的指令地址存储在名为栈“的主存内。函数处理完毕后,再通过函数的出口来执行return命令。return命令的功能是把保存在栈中的地址设定到程序计数器中。如图1-7所示,MyFunc 函数被调用之前,0154地址保存在栈中。MyFunc函数的处理完毕后,栈中的0154地址就会被读取出来,然后再被设定到程序计数器中(图1-8 )。
1.6 通过地址和索引实现数组
通过基址寄存器和变址寄存器,我们可以对主内存上特定的区域进行划分,从而实现类似于数组的操作
首先,我们用十六进制数将计算机内存上00000000 -FFFFFFFF的地址划分出来。那么,凡是该范围的内存区域,只要有一个32位的寄存器,即可查看全部的内存地址。但如果想要像数组那样分割特定的内存区域以达到连续查看的目的,使用两个寄存器会更方便些。例如,查看10000000地址~100OFFFF地址时,如图1-9所示,可以将10000000存入基址寄存器,并使变址寄存器的值在00000000~0000FFFF变化。CPU则会把基址寄存器+变址寄存器的值解释为实际查看的内存地址。变址寄存器的值就相当于高级编程语言程序中数组的索引功能。
数组是指同样长度
的数据在内存中进行连续排列的数据构造。用一个数组名来表示全体数据,通过索引来区分数组的各个数据(元素)。例如,一个10个元素的数组a,其中的各个数据就用a[0]~a[9]来表示。[内的数字0一9就是索引。
十六进制:二进制数的位数较多、不易理解时,通常使用十六进制数来代替二进制数。这是一种数到16就进位的计数方式。我们用A-F来分别表示10~15,那么,二进制数的4位(0000~FFFF)就可以用十六进制数的1位(0~F)来表示。32位的二进制数,就可以用8位的十六进制数来表示。
1.7 CPU的处理其实很简单
外围设备指的是连接到计算机的键盘、鼠标、显示器、设备装置、打印机等。
第二章:数据是用二进制数表示的
问题
1 :32位是多少字节?
答:4四节(8位=1字节)。
2:二进制数01011100转换成10进制数是多少?
92
3:二进制数左移动两位后,会变成原数的几倍?
答:4倍,左移一位是原数的2的一次方倍,左移两位是原数的2的2次方倍。
4:补码形式表示的8位二进制数11111111,用十进制数表示是多少?
答:-1。
5:补码形表示的8位二进制数10101010,用16位的二进制数表示的话是多少?
1111111110101010(使用原数的最高位1来填充高位)
6:反转部分图形模式时,用的是什么逻辑运算?
答:XOR运算。(XOR运算只反转与1相对应的位,NOT运算反转所有的位)
本章重点
在C,JAVA等高级语言编写的程序中,数值,字符串和图像等信息在计算机中都是以二进制数值的形式来表现的。掌握使用二进制数表示信息的方法及其运算机制,能更好地理解程序的运行机制。
2.1 用二进制数表示计算机信息的原因
- 计算机内部由IC(集成电路)这种部件构成,CPU和内存也是IC的一种
- IC的所有引脚,只有直流电压0V或5V两个状态。(即IC的一个引脚只能表示两个状态)(大部分IC的电源电压都是+5V,但是为了控制电量消耗,有时也会使用+5V之下的电压,这是引脚状态不只是0V和+5V,还存在不接受电流信号的高阻抗状态。)
- IC分为模拟IC和数字IC
- IC的一个引脚表示二进制数的1位
- 位:计算机处理信息的最小的单位(最小单位)
- 字节:最基本的信息计量单位(基本单位)(内存和磁盘都是使用字节单位来存储和读写数据,用位单位无法读写数据)
用字节单位处理数据时,如果数据小于存储的字节数,那么高位就用0填补
奔腾等32位微处理器。具有32个引脚以用于信息的输入和输出,也就是说,奔腾一次可以处理32位(4字节)的二进制数信息
程序中,即使用十进制和文字等记述信息,在编译后也会转换成二进制数的值,所以,程序运行时,计算机内部处理也是用二进制数表示的信息
对于用二进制数表示的信息,计算机不会区分它是数值、文字,还是某种图片的模式等,而是根据编写程序的各位对计算机发出的指示来进行信息的处理(运算)。例如00100111这样的二进制数,既可以视为纯粹的数值作加法运算,也可以视为“”(单引号,singlequotation)文字而显示在显示器上,或者视为■■口■■口口口这一图形模式印刷出来。具体进行何种处理,取决于程序的编写方式。
2.2 什么是二进制数
例如,十进制数39的各个数位的数值,并不只是简单的3和9,3表示的是3×10 = 30,9表示的是9×1= 9。这里和各个数位的数值相乘的10和1,就是位权。数字的位数不同,位权也不同
。第1位(最右边的一位)是10的0次幂“(= 1 ),第2位是10的1次幂(=10),第3位是10的2次幂(= 100),依此类推
位权的思考方式也同样适用于二进制数。即第1位是2的0次幂( =1),第2位是2的1次幂(=2),第3位是2的2次幂( =4 ),…….,第8位是2的7次幂( = 128 )。“OO的××次幂”表示位权,其中,十进制数的情况下OO部分为10,二进制数的情况下则为2。这个称为基数
。十进制数是以10为基数的计数方法,二进制数则是以2为基数的计数方法。“OO的××次幂”中的××,在任何进制数中都是“数的位数-1”。即第1位是1-1=0次幂,第2位是2-1=1次幂,第3位是3-1=2次幂。
二进制数00100111用十进制数表示的话是39,因为(0×128 )+ ( 0×64)+ ( 1×32)+(0×16)+(0×8 ) +( 1×4) +( 1×2)+(1×1)=39。
总结:二进制数是以2为基数,用0和1两个数码来表示的数,0,1为算符,二进制数的位权是2的幂
为什么不同进制的数字通过位权的思想相加之后,都能准确地表示10进制中的一个数字呢?
在位权思想的大前提下,我们印象中的乘法和加法,都是基于十进制而实现的。这只不过是用十进制来表现位权的思想罢了
2.3 移位运算符和乘除运算的关系
移位运算是指将二进制数的各数位进行左右移位
移位运算也可以通过数位移动来代替乘法运算和除法运算。例如,将00100111左移两位的结果是10011100,左移两位后数值变成了原来的4倍。用十进制数表示的话,数值从39 ( 00100111)变成了156 ( 10011100),也正好是4倍(39×4= 156 )。
十进制数左移后会变成原来的10倍、100倍、1000倍……同样,二进制数左移后就会变成原来的2倍、4倍、8倍……反之,二进制数右移后则会变成原来的1/2、1/4、1/8……所以移位运算能代替乘法运算和除法运算
右移的具体运算参见 https://www.baidu.com/baidu?tn=monline_4_dg&ie=utf-8&wd=9.75%E7%9A%84%E4%BA%8C%E8%BF%9B%E5%88%B6
2.4 便于计算机处理的补数
二进制数中表述负数时,一般会把最高位作为符号来使用,因此,我们把这个最高位叫做符号位,符号位为0表述正数,为1表示负数
计算机在做减法运算时,实际上内部是在做加法运算。为此,在表示负数时就需要使用“二进制的补数”。补数就是用正数来表示负数
为了获得补数,我们需要将二进制数的各数位的数值全部取反、然后再将结果加1即取反加1。例如,用8位二进制数表示-1时,只需求得1,也就是00000001的补数即可。具体来说,就是将各数位的0取反成1,1取反成0,然后再将取反的结果加1,最后就转化成了11111111
例如1-1,也就是1+(-1)这一运算,我们都知道答案应该是0。首先,让我们将-1表示成10000001(错误的表示方法)来运算,看看结果如何。00000001 +10000001 = 10000010,很明显结果不是0(图2-6)。如果结果是0,那么所有的数位都应该是0才对。
接下来,让我们把-1表示成11111111(正确的表示方法)来进行运算。00000001 +11111111确实为0( = 00000000)。这个运算中出现了最高位溢出的情况,不过,正如之前所介绍的那样,对于溢出的位,计算机会直接忽略掉。在8位的范围内进行计算时,100000000这个9位二进制数就会被认为是00000000这一8位二进制数(图2-7 )。
法则:将二进制取反后,和原来的值相加,结果为0
比如3-5这个运算,用8位二进制数表示3时为00000011,而5 = 00000101的补数为“取反+1”,也就是11111011。因此3-5其实就是00000011 +11111011的运算。
3-5=3+(-5)
编程语言包含的整数数据类型“中,有的可以处理负数,有的则不能处理。例如,C语言的数据类型中,既有不能处理负数的unsignedshort类型,也有能处理负数的short类型。这两种类型,都是2字节(=16位)的变量,都能表示2的16次幂=65536种值,这一点是相同的。不过,值的范围有所不同,short类型是-32768~32767,unsignedshort类型是0~65535。此外,short类型和unsigned short类型的另一个不同点在于,short类型是将最高位为1的数值看作补数,而unsigned short类型则是32768以上的值。
为什么short类型取值范围为-32768~32767
如果以最高位为符号位,二进制原码最大为0111111111111111=2的15次方减1=32767
最小为1111111111111111=-2的15次方减1=-32767
仔细思考一下补数的机制,大家就会明白像-32768~32767这样负数比正数多一个的原因了。最高位是0的正数,有0~32767共32768个,这其中也包含0。最高位是1的负数,有-1~32768共32768个,这其中不包含0。也就是说,0包含在正数范围内,所以负数就要比正数多1个。虽然0不是正数,但考虑到符号位,就将其划分到了正数中。
2.5 逻辑右移和算术右移的区别
右移有移位后在最高位补0
和补1
两种情况。当二进制数的值表示图形模式而非数值时,移位后需要在最高位补0。类似于霓虹灯往右滚动的效果。这就称为逻辑右移(图2-9 )。
将二进制数作为带符号的数值进行运算时,移位后要在最高位填充移位前符号位的值(0或1)。这就称为算术右移。如果数值是用补数表示的负数值,那么右移后在空出来的最高位补1,就可以正确地实现1/2、1/4、1/8等的数值运算。如果是正数,只需在最高位补0即可。(即用移位前的符号位填充)
将 -4(=11111100)右移两位。这时,逻辑右移的情况下结果就会变成00111111,也就是十进制数63,显然不是-4的1/4。而算术右移的情况下,结果就会变成11111111,用补数表示就是-1,即-4的1/4(图2-10 )。
只有在右移时才必须区分逻辑位移和算术位移。左移时,无论是图形模式(逻辑左移)还是相乘运算(算术左移),都只需在空出来的低位补0即可。
二进制运算中为什么0-1=1?
如果只考虑本位的话,0-1=1,这就像十进制中只考虑当前本位时,2-8=4。当然,这是以从高位借到了一个1(以一当十)为前提的。同样,在二进制中,0-1=1,也是以从高位借到了一个1(以一当二)为前提的。如果只有1位,没有什么高位,那么,0-1=-1!就像在十进制中,2-8=-6。
怎么求补码的原码?
已知一个数的补码,求原码的操作其实就是对该补码再求补码:
1、如果补码的符号位为“0”,表示是一个正数,其原码就是补码。
2、如果补码的符号位为“1”,表示是一个负数,那么求给定的这个补码的补码就是要求的原码。
例如:已知一个补码为11111001,则原码是10000111(-7)。因为符号位为“1”,表示是一个负数,所以该位不变,仍为“1”。其余七位1111001取反后为0000110;再加1,所以是10000111。
符号扩充
以8位二进制数为例,符号扩充就是指在保持值不变的前提下将其转换成16位和32位的二进制数。将01111111这个正的8位二进制数转换成16位二进制数时,很容易就能得出0000000001111111这个正确结果,但是像11111111这样用补数来表示的数值,该如何处理比较好呢?实际上处理方法非常简单,将其表示成1111111111111111就可以了。也就是说,不管是正数还是用补数表示的负数,都只需用符号位的值(0或者1)填充高位即可。
2.6掌握逻辑运算的窍门
计算机能处理的运算,大体可分为算术运算
和逻辑运算
。
算术运算
是指加减乘除四则运算。
逻辑运算
是指对二进制数各数字位的0和1分别进行处理的运算,包括逻辑非( NOT运算)、逻辑与(AND运算)、逻辑或(OR运算)和逻辑异或( XOR运算)四种。
逻辑非指的是0变成1、1变成0的取反操作。
逻辑与指的是“两个都是1”时,运算结果为1,其他情况下运算结果都为0的运算。
逻辑或指的是“至少有一方是1”时,运算结果为1,其他情况下运算结果都是0的运算。
逻辑异或指的是排斥相同数值的运算。“两个数值不同”,也就是说,当“其中一方是1,另一方是0”时运算结果是1,其他情况下结果都是0。
不管是几位的二进制数,在进行逻辑运算时,都是对相对应的各数位分别进行运算。
给小学生讲CPU和二进制
通电后,cpu的每个引脚会有电流通过,通过电流信号,我们就可以给cpu发送指令或者传递数字信息,比如说,让电脑计算1+2的时候,就要把进行加法计算的命令和1和2这两个数字传递给CPU。
CPU的引脚有电流通过时,数值为1,没有电流通过的时候数值为0,这是CPU 里的规定。
第三章:计算机进行小数运算时出错的原因
问题
1: 二进制数0.1,用十进制数表示的话是多少?
答:0.5 = 2的-1次方。
2: 用小数点后有3位的二进制数,能表示十进制数0.625吗?
答:能。0.101。
3:将小数分为符号、尾数、基数、指数4部分进行表现得形式称为什么?
答:浮点数。(符号 位数 * 基数的指数次幂)
4: 二进制数的基数是什么?
答:2。
5: 通过把0作为数值范围的中间值,从而在不适用符号位的情况下来表示负数的表示方法称为什么?
答:EXCESS系统表现。(EXCESS是剩余的意思)
6: 10101100.01010011这个二进制数,用十六进制数来表示的话是什么?
答:AC.53。要点。二进制的4位相当于十六进制的1位。
本章重点
实际上,万能的计算机也会存在程序运行后无法得到正确数值的情况,小数运算就是一个典型的例子
3.1 将0.1累加100次也得不到10
0.1累加100次得到10.000002
3.2用二进制表示小数
位权是用来与各数字为的数字相乘的数值
3.3计算机运算出错的原因
计算机运算出错的原因是有一些十进制的小数无法转换成二进制数
例如,十进制数0.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 ),计算机运算出错的原因也是同样的道理。
3.4 什么是浮点数
像1011.0011这样带小数点的表现形式,完全是纸面上的二进制数表现形式,在计算机内部是无法使用的。
很多编程语言中都提供了两种表示小数的数据类型,分别是双精度浮点数
和单精度浮点数
。双精度浮点数类型用64位、单精度浮点数类型用32位来表示全体小数。
浮点数是指用符号、尾数、基数和指数这四部分来表示的小数。因为计算机内部使用的是二进制数,所以基数自然就是2。因此,实际的数据中往往不考虑基数,只用符号、尾数、指数这三部分即可表示浮点数。也就是说,64位(双精度浮点数)和32位(单精度浮点数)的数据,会被分为三部分来使用
像0.12345×103和0.12345 ×10-1这样使用与实际小数点位置不同的书写方法来表示小数的形式称为浮点数。与浮点数相对的是定点数,使用定点数表示小数时,小数点的实际位置固定不变。例如,0.12345×103和0.12345 ×10-1用定点数来表示的话即为123.45和0.012345。
尾数部分和指数部分并不只是单单存储着用整数表示的二进制数。尾数部分用的是“将小数点前面的值固定为1的正则表达式,而指数部分用的则是“EXCESS系统表现”。
3.5 正则表达式和EXCESS系统
按照特定的规则来表示数据的形式即为正则表达式。除小数之外,字符串以及数据库等,也都有各自的正则表达式。
整数是指使用包含表示符号的最高位在内的全体来表示的一个数值。而浮点数是由符号部分、尾数部分和指数部分这三部分独立的数值组合而成的。
为了方便计算机处理,需要制定一个统一的规则。例如,十进制数的浮点数应该遵循“小数点前面是0,小数点后面第1位不能是0”
这样的规则。根据这个规则,0.75就是“0.75×10的0次幂”,也就是说,只能用尾数部分是0.75、指数部分是0这个方法来表示。根据这个规则来表示小数的方式,就是正则表达式。
在二进制数中,我们使用的是“将小数点前面的值固定为1的正则表达式”。具体来讲,就是将二进制数表示的小数左移或右移(这里是逻辑移位。因为符号位是独立的)数次后,整数部分的第1位变为1,第2位之后都变为0(这样是为了消除第2位以上的数位)。而且,第1位的1在实际的数据中不保存。由于第1位必须是1,因此,省略该部分后就节省了一个数据位,从而也就可以表示更多的数据范围(虽不算太多)。
单精度浮点数,尾数部分是23位,由于第一位的1被省略了,实际可以表示24位的数值
接下来,让我们一起来看一下指数部分中使用的EXCESS系统,使用这种方法主要是为了表示负数时不使用符号位
。在某些情况下,在指数部分,需要通过“负○○次幂”的形式来表示负数。EXCESS系统表现是指,通过将指数部分表示范围的中间值设为0,使得负数不需要用符号来表示。也就是说,当指数部分是8位单精度浮点数时,最大值11111111 = 255的1/2,即01111111=127(小数部分舍弃)表示的是0,指数部分是11位双精度浮点数时,11111111111 = 2047的1/2,即01111111111 =1023(小数部分舍弃)表示的是0。
EXCESS系统可能不太好理解,下面举例来说明。假设有这样一个游戏,用1~13(A~K)的扑克牌来表示负数。这时,我们可以把中间的7这张牌当成0。如果扑克牌7是0,10就表示+3,3就表示-4。事实上,这个规则说的就是EXCESS 系统
。
作为单精度浮点数的示例,表3-2中,指数部分为二进制数11111111(十进制数255),那么在 EXCESS系统中则表示为128次幂。这是因为255-127= 128。因此,8位的情况下,表示的范围就是-127次幂~128次幂。
3.6 在实际的程序中进行确认
3.7 如何避免计算机计算出错
一个策略是把小数转换成整数来计算。计算机在进行小数计算时可能会出错,但进行整数计算(只要不超过可处理的数值范围)时一定不会出现问题。因此,进行小数的计算时可以暂时使用整数,然后再把计算结果用小数表示出来即可。将0.1相加100次这一计算,就可以转换为将0.1扩大10倍后再将1相加100次的计算,最后把结果除以10就可以了
BCD ( Binary Coded Decimal)也是一种使用二进制表示十进制的方法。简单来讲,BCD就是用4位来表示0~9的1位数字的处理方法,在涉及财务计算等不允许出现误差的情况下,一定要将小数转换成整数或者采用BCD方法,以确保最终得到准确的数值。
3.8 二进制数和十六进制数
在以位为单位表示数据时,使用二进制数很方便,但如果位数太多,看起来就比较麻烦。因此,在实际程序中,也经常会用十六进制数来代替二进制数。在C语言程序中,只需在数值的开头加上Ox ( 0和x)就可以表示十六进制数。
二进制数的4位,正好相当于十六进制数的1位。例如,32位二进制数00111101110011001100110011001101用十六进制数来表示的话,就是3DCCCCCD这个8位数。由此可见,通过使用十六进制数,二进制数的位数能够缩短至原来的1/4。位数变少之后,看起来也就更清晰
用十六进制数来表示二进制小数时,小数点后的二进制数的4位也同样相当于十六进制数的1位。不够4位时用0填补二进制数的低位即可。例如,1011.011的低位补0后为1011.0110,这时就可以表示为十六进制数B.6(图3-10)。十六进制数的小数点后第1位的位权是16"即 1/16=0.0625
第四章:熟练使用有棱有角的内存
问题
1.有十个地址信号引脚的内存IC(集成电路)可以指定的地址
范围是多少?
用二进制数来表示的话是0000000000 -1111111111(用十进制数来表示的话是0-1023 )
2.高级编程语言中的数据类型表示的是什么?
占据内存区域的大小和存储在该内存区域的数据类型(比如short,表示占据2字节内存区域,并且存储整数)
3.在32位内存地址的环境中,指针变量的长度是多少位?
32位,指针是用来存储内存地址的变量
4.与物理内存有着相同构造的数组的数据类型长度是多少?
1字节,物理内存以字节为单位
5.用LIFO方式进行数据读写的数据结构称为什么?
栈
6.根据数据的大小链表分叉成两个方向的数据结构称为什么?
二叉查找树( binary search tree )
本章重点
计算机是进行数据处理的设备,而程序表示的就是处理顺序和数据结构。由于处理对象数据是存储在内存和磁盘上的,因此程序必须能自由地使用内存和磁盘。只要在程序上花一些心思,就可以将内存变换成各种各样的数据结构来使用。
4.1 内存的物理机制很简单
内存实际上是一种名为内存IC的电子元件。虽然内存IC包括DRAM、SRAM、ROM等多种形式,但从外部来看,其基本机制都是一样的。内存IC中有电源、地址信号、数据信号、控制信号等用于输入输出的大量引脚(IC的引脚),通过为其指定地址( address ),来进行数据的读写。
ROM( Read Only Memory )是一种只能用来读取的内存。
RAM (Random Access Memory )是可被读取和写入的内存,分为需要经常刷新( refresh )以保存数据的DRAM ( Dynamic RAM ),以及不需要刷新电路即能保存数据的SRAM ( Static RAM ).
数据信号引脚有D0-D7共八个,表示一次可以输入输出8位(=1字节)的数据。此外,地址信号引脚有A0~A9共十个,表示可以指定0000000000~1111111111共1024个地址。而地址用来表示数据的存储场所,因此我们可以得出这个内存IC中可以存储1024个1字节的数据。因为1024=1K,所以该内存IC的容量就是1KB。
在计算机领域,大写字母K表示的并不是1000,而是2的10次幂的结果1024。1000通常用小写k来表示。
通常情况下,计算机使用的内存IC中会有更多的地址信号引脚,这样就能在一个内存IC中存储数十兆字节的数据。因此,只用数个内存IC,就可以达到512MB或者更多的容量。
假设要往该内存IC中写入1字节的数据。为了实现该目的,可以给VCC接入+5V,给GND接入0V的电源,并使用A0-A9的地址信号来指定数据的存储场所,然后再把数据的值输入给D0~D7的数据信号,并把 WR ( write = 写入的简写)信号设定成1。执行完这些操作,就可以在内存IC内部写入数据
读出数据时,只需通过A0~A9的地址信号指定数据的存储场所,然后再将RD ( read =读出的简写)信号设成1即可。执行完这些操作,指定地址中存储的数据就会被输出到DO~D7的数据信号引脚
像WR和RD这样可以让IC运行的信号称为控制信号。其中,当WR和RD同时为0时,写入和读出的操作都无法进行。
总体来讲,内存IC内部有大量可以存储8位数据的地方,通过地址指定这些场所,之后即可进行数据的读写。
4.2 内存的逻辑模型是楼房
可以把内存IC模拟成楼房,每一层可以存储一个字节的数据,1Kb就是1024层
数据类型:存储何种类型的数据,虽然物理上是以1个字节为单位来注意读写数据的内存,在程序中,通过指定变量的数据类型,也能实现以特定字节数为单位进行读写 ,(不然的话处理超过1字节的数据时还得写分割程序,这非常不方便)
通过使用变量,即便不指定物理地址,也可以在程序中对内存进行读写,因为程序运行时,操作系统会自动决定变量的物理地址
将多字节数据的低位字节存储在内存低位地址的方式称为低字节序
与此相反,把数据的高位字节存储在内存低位的方式称为高字节序
比如以下C程序
//定义变量
char a;//1个字节
short b;//2个字节
long c;//4个字节
//给变量赋值
a=123;
b=123;
c=123;
4.3简单的指针
指针也是一种变量,它所表示的不是数据的值,而是存储着数据的内存的地址,通过指针,就可以对任意地址的数据进行读写
windows计算机使用的程序通常都是32位(4个字节)的内存地址,这种情况下,指针变量的长度也是32位
定义指针时,通常在变量名前面加一个*
char *d;// char类型的指针d的定义
short *e;// short类型的指针e的定义
long *f ;// long类型的指针王的定义
在上面的代码中,d、e、f都是用来存储32位(4字节)的地址的变量。用来指定char(1字节)、short (2字节)、long ( 4字节)这些数据类型,这些数据类型表示的是从指针存储的地址中一次能够读写的数据字节数。
假设d、e、f的值都是100。在这种情况下,使用d时就能够从编号100的地址中读写1个字节的数据,使用e时就是2个字节(100地址和101地址)的数据,使用f时就是4个字节(100地址~103地址)的数据。
4.4 数组是高效使用内存的基础
数组是指多个同样数据类型的数据在内存中连续排列的形式。作为数组元素的各个数据会通过连续的编号被区分开来,这个编号称为索引( index )。指定索引后,就可以对该索引所对应地址的内存进行读写操作”(CPU通过基址寄存器和变址寄存器来指定内存)。而索引和内存地址的变换工作则是由编译器自动实现的。
char g[100];// char类型数组g的定义
short h[100];// short类型数组n的定义
long i [100] ;// long 类型数组i的定义
数组的定义中所指定的数据类型,也表示一次能够读写的内存大小
使用数组能够使编程工作变得更加高效。如果在反复运行的循环处理“中使用数组,很短的代码就能达到按顺序读出或写入数组元素的目的。不过,虽然是通过指定索引来使用数组,但这和内存的物理读写并没有特别大的区别。
4.5 栈、队列以及环形缓冲区
栈和队列都可以不通过指定地址和索引来对数组元素进行读写
栈:先入后出
队列:先入先出
队列这一方式也称为排队。排队指的是买车票时在自动售票机前等候的队列等。排队时,站在最前面的乘客先买票,购买后率先从队列中走出来。当随机前来的购票乘客数量和自动售票机的处理速度不相符时,排队能起到很好的缓冲作用。程序中也是如此,为了协调好数据输入和处理时机间的关系,采用类似于排队的机制是很方便的。在内存上,实现这种机制的方式就是队列。当我们需要处理通讯中发送的数据时,或由同时运行的多个程序所发送过来的数据时,会用到这种对队列中存储的不规则数据进行处理的方法。
假设我们要用有6个元素的数组来实现一个队列。这时可以从数组的起始位置开始有序地存储数据,然后再按照存储时的顺序把数据读出。在数组的末尾写入数据后,后一个数据就会被写入数组的起始位置(此时数据已经被读出所以该位置是空的)。这样,数组的末尾就和开头连接了起来,数据的写入和读出也就循环起来了
4.6 链表使元素的追加和删除更加容易
在数组的各个元素中,除了数据的值之外,通过为其附带上下一个元素的索引,即可实现链表。数据的值和下一个元素的索引组合在一起,就构成了数组的一个元素。
链表删除元素很方便,只需要改变当前元素的下一个指向
当第2个元素的下一个元素变成第4个元素后,那么第3个元素就被删除了。虽然第3个元素在物理内存上还残留着,但在逻辑上则确实被删除了
如果不使用链表数组,那么中途删除或追加元素时,其后的元素就必须要全部移动,使用链表插入和删除元素更节约时间
4.7 二叉查找树使数据搜索更有效
二叉查找树“是指在链表的基础上往数组中追加元素时,考虑到数据的大小关系,将其分成左右两个方向的表现形式。例如,假设我们事先把50这个值保存到了数组中。那么,如果接下来的值比先前保存的数值大的话,就要将其放到右边,反之如果小的话就放在左边。但实际的内存并不会分成两个方向,这是在程序逻辑上实现的
为了实现二叉查找树,怎么处理比较好呢?
其实数组的每个元素中只要有数据的值和两个索引信息就可以了。二叉查找树是由链表构造发展而来的表现形式,因此在追加或删除元素方面也同样是有效的。
使用二叉查找树的便利之处在于可以使数据的搜索等更有效率。在使用一般的数组时,必须从数组的开头按照索引顺序来查找目标数据。而使用二叉查找树时,当目标数据比现在读出来的数据小时就可以转到左侧,反之目标数据较大时即可转到链表的右侧,这样就加快了找到目标数据的速度。
还原为树就是如下图
只要在程序开发中多花一些心思,我们就可以熟练地使用内存、实现栈处理、链表处理、二叉查找树处理等=不过,大家还必须理解为什么要进行这些处理。另外,请大家牢记数组是进行这些处理的基础
。