深入理解计算机系统(2.1)---信息的存储与值的计算
上一章(1.1-1.4)LZ和各位简单的探讨了一下计算机系统中的一些基本概念,本次我们将进入一个崭新的世界。在1.1那一章当中,我们已经简单的提及了信息的概念,本次我们会逐渐深入的讨论信息的相关内容。
引言
我们很难想象,1和0这两个再简单不过的数字,给计算机科学带来了彻底的改变。对于无法与人脑相比的计算机来说,简单的1和0就是最适合它们的数字。不过1个1或者1个0往往代表不了任何意义,它们必须被赋予上下文,才能有具体的含义。比如,如果我们知道1和0是代表的布尔类型的值,那么我们就知道1是true,0是false。
对于二进制所表示的数字来说,主要有三种,即无符号、补码以及浮点数。
由于计算机对于固定类型的二进制数字往往都是有位数限制的,比如int类型使用四个字节(32位二进制)来表示,因此在计算的时候,会发生溢出的情况,最简单的我们使用无符号整数0xFFFFFFFF与无符号整数0xFFFFFFFF相乘,则会产生溢出。
在产生溢出的时候,得出的结果往往会令人大跌眼镜。比如两个正数相乘可能得到负值,两个正数相加也可能得到负值。而对于不同的计算机来说,由于数值范围可能有所不同,因此掌握信息相关的内容对于写出跨平台的程序来讲也是很有帮助的。
信息的存储
大多数计算机使用8位的块,或者说字节,来作为最小的可寻址的存储器单位,而不是在存储器中访问单独的位。换句话说,我们在访问存储器的内容时,最小的访问单位一般是字节。
在我们编程的时候,往往会把虚拟存储器(virtual memory)抽象为一个字节数组,而每一个数组内的元素或者说字节都有唯一的地址(address),这些地址的集合就被称作虚拟地址空间。虚拟地址空间是为了给机器级的程序一个概念上的映像,实际上为了提供这个映像,内部的实现是非常复杂的。
十六进制表示法
对于机器来说,可能比较喜欢0和1,但是对于人类这种高级生物来说,1和0就有点不够看了。因此通常情况下,为了便于阅读,我们会使用十六进制去表示二进制。1位十六进制的数字可以表示4位二进制数字,因此一个字节就可以表示为0x00---0xFF。
有关十六进制、二进制以及十进制的转换,LZ这里就直接略过了,相信大部分人应该都对这个转换并不陌生。
字
每台计算机都有一个字长(word size),用于指明整数和指针数据的标准大小(nominal size)。而由于虚拟地址空间中的地址就是使用一个字来表示的,因此操作系统中的字长就决定了虚拟地址空间的大小。
比如32位操作系统下,最大内存就是232 = 4 * 210 * 210 * 210 B = 4GB,而在64位操作系统下,LZ还专门问了问群里的猿友,最终得到的结果是234GB。
数据大小
由于计算机位数的不同,会造成在数据类型的存储上,采用的位数略有不同,下表是在32位和64位机器下,C语言当中的数字数据类型需要的位数。
可以看出,对于长整形以及字符指针类型来说,在32位和64位系统下的字节数是不同的。特别的,对于指针类型来说,所有指针类型在32位下都是4位,而在64位下都是8位,这是由虚拟地址空间的地址位数或者说字长所决定的。
寻址和字节顺序
对于跨越多个字节的程序对象(程序对象指指令、数据或者控制信息等,是程序当中对象的统称)来说,我们需要制定两个规则,才能唯一确定一个程序对象的值。
比如对于int类型的值0xFF来说,如果我们要根据虚拟内存地址去获取这个整数值,那么首先我们需要知道它的起始虚拟内存地址是多少。另外,我们还需要知道,对于表示int类型的四个字节来说,这四个字节的排列顺序。
对于第一个问题,由于大部分计算机都采用连续的内存地址去存储一个程序对象,因此我们称内存地址中最小的那个就是该程序对象的起始地址,也是该程序对象的地址。
对于第二个问题,一般有两种方式,即大端法和小端法。对于一个整数0x000000FF来说,我们假设它的起始地址为0x1,那么对于使用大端法规则的系统来说,0x1-0x4的虚拟内存所存储的值依次为0x00、0x00、0x00、0xFF,相反对于采用小端法规则的系统来说,0x1-0x4的虚拟内存所存储的值依次为0xFF、0x00、0x00、0x00。
强制类型转换
对于一个特定的数据类型来讲,计算机在解释这类数据的值的时候,是根据起始位置以及数据类型的位数来确定的。比如对于无符号int类型的数据来说,倘若我们知道它的起始位置为0x1,而当前的操作系统采取的是大端法规则,假设0x1-0x4的内存地址中存储的字节依次为0xFF,0xFF,0xFF,0xFF,由此计算机将会帮我们计算出这个无符号int类型的值为232-1,也就是无符号int类型的最大值。
这其中计算机是根据0x1-0x4这四个字节上的值,以及无符号int类型的解释方式,最终得到的这个无符号int类型的值。
由此可见,计算机在解释一个数据类型的值时主要有四个因素:位排列规则(大端或者小端)、起始位置、数据类型的字节数、数据类型的解释方式。
对于特定的系统来说,前两种因素都是特定的,而对于后两种因素的改变,则可以改变一个数据类型的值的最终计算结果,这就是强制类型转换。对于大部分高级程序设计语言来讲,都提供了强制类型转换。
比如C语言,我们可以将一个无符号int类型的值强制转换为其它类型,在转换之后,对于上面四个因素之中,改变的是最后两个。为此我们写一个小程序来看下这个有意思的事情。
#include <stdio.h>
int main(){
unsigned int x = 0xFFFFFF61;
int *p = &x;
char *cp = (char *)p;
printf("%c\n",*cp);
}
这是一个简单的强制类型转换示例,可以看到我们将一个无符号int类型的值,先赋给了一个int类型的指针,又强制转换成了char类型的指针,最终我们输出这个char类型指针所代表的字符,结果的输出是一个a。
输出a的原因就是由上面的四个因素决定的,我们看这个具体程序上的四个因素。
1、cp指针的值与x变量的起始内存地址相等。(起始位置)
2、LZ的linux系统是小端表示法,也就是说假设x变量的起始内存地址为0x1,那么0x1-0x4的值分别为0x61、0xFF、0xFF、0xFF。(位排列规则)
3、char只占一个字节,因此会只读取0x61这个值。(数据类型的字节数,或者说大小)
4、0x61为十进制的97,对应ascii表的话,代表的是字符a,因此最终输出了a。(数据类型的解释方式)
可以看出,强制类型转换有时候会让结果变的让人难以预料,因此这种技巧一般不太推荐使用,但是这种手段也确实是程序设计语言所必需的。
字符串的表示
这一点其实上面我们已经提到了,我们知道97其实代表的是字符'a',而这个的由来就是根据ascii表来的,我们在linux系统上可以输入man ascii命令来查看。
代码的表示
二进制如何表示代码?
其实这些都是编译器的责任了,我们只需要写出像上面那个小程序一样的人们可以看懂的代码,编译器便会帮我们将其翻译成对应的机器所认识的二进制序列。从这个角度上来讲,程序语言其实就是一个二进制序列的简单描述,它提供我们更简单的编写计算机可以执行的二进制序列的方式。
文章小结
本次我们初步探索了信息的存储以及信息所代表的结果的计算,这些内容都比较基础,相信不难看懂。