信息的表示和处理

这是《深入理解计算机系统》(第二版)的第二章,原文大概有60页的篇幅,主要讲述各种类型的信息是如何在计算机系统中表示并处理的。这么多的内容,一篇博客是很难详尽的,所以本文就摘取了其中个人认为比较重要的部分,加上个人的一些理解讲给大家,希望可以给新人一些帮助,能起到一点儿抛砖引玉的作用。

前言

再次强调,计算机系统中的所有信息都是以二进制形式存储的,系统以二进制的记载形式表示着大千世界的所有信息。其中数字是最重要的消息,根据人们的各种需求,数字分为各种种类:正数、负数、有理数、无理数、整数、小数等等。这些数字如何在计算机系统中表示呢?为了描述刻画这些数字,计算机系统规定了三种重要的编码方式:无符号编码、补码编码、浮点数编码。研究这些编码形式是如何和现实世界中的数字对应以及对应过程中可能会出现的问题是本章研究的重点。

为了给大家一个感性的认识,在进入正文之前先说一个小知识点。在编程过程中,判断某个变量是不是0是非常常见的。但是,把一个变量和0比较的方法确不总是像if(a==0)这样简单,因为如果a是一个浮点数,那这种比较几乎是没有意义的!要拿一个浮点数和0比较,那这个浮点数肯定是一个表达式或者一系列表达式的求值结果,毕竟谁也不会刚定义一个double a=0.0;紧接着就去判断a是不是等于0。而浮点数运算的表达式结果是不太可能出现0这种结果的,当其理论值是0时,它的结果也会是0.0000000001这种形式,所以判断一个浮点数是不是0的一般做法是if(fabs(a)<0.0000001)(原谅我没有用指数形式)这种形式。这里隐含着一个事实,那就是虽然整数的表示范围相对较小,但表示的数字是精确的,计算机系统中的二进制编码是对整数的完全表示,编码过程没有损失任何信息。而浮点数虽然表示的范围比较大,有些时候却是不精确的,即无论多大精度的浮点数都无法精确的表示1/3这种数字,编码过程不可避免的损失了一些信息。

1. 信息存储

在前面一篇文章中我知道,计算机系统把整个内存和各种I/O设备抽象成虚拟存储器,也就是一片连续字节数组。这里说一下C程序员感触最深的指针,指针存放的是一个变量的地址,这个地址其实是虚拟内存中的地址而并不是真实的地址,也就是把这个地址值翻译成32位(以32位机器为例)二进制代码,然后把它们对应成高低电平去总线上读数据,读到的并不是你想要的那个变量的数据。当然,这些东西你几乎永远不用考虑,而你不用考虑的原因就是操作系统实现了虚拟内存这个抽象概念,把你的工作减轻了很多。

既然说到了指针,那就有必要了解一下再有一点让很多新手搞不明白的是“数据类型”的概念,虽然绝大部分语言书籍的第一章就是讲的数据类型,却很少有人对它提起足够的重视,导致很多关键概念不能理解。这里我给大家强调一下,程序中的数据是有类型的,这体现在很多地方。举个例子,int *p1,与int (*p2)[10];p1和p2是两个指针变量,它们各有一个值,就是它们指向的内容,这个大家都非常熟悉了。但正如大家同样熟悉的,p1+1与p2+1的结果是不同的,p1+1是跳了sizeof(int)个字节,而p2+1跳的是10*sizeof(int)个字节。这就是因为p1和p2的类型不同,所以同一个表达式的作用效果却不同。再来一个,在你的编译中试一下cout<<sizeof(4)<<" "<<sizeof(4.0)<<endl;同样是4,但占的字节大小时不同的!就是因为程序中的4与4.0的数据类型不同。再来最后一个,char *p = "abcd";现在问你,p是什么?p是一个指针,那输出一个指针会是什么结果?会输出“abcd”这个字符串,那如果是int *p = new int(10);再输出p,结果是什么?结果是一个地址。那为什么同样是指针,一个输出的是指针指向你内容,而一个输出的是指针本身的值?没错,就是因为指针的类型是不同的,相比指针的值来说,指针的类型是更加重要的概念。而不同的数据类型在同样的语句中有不同的表现是很多新人不能理解的,再去想一下,p、&p、&&p、*p、**p之间有什么关系?

既然数据类型这么重要,那我们就来给数据类型下一个定义,在数据结构一类的书中,数据类型被定义为“一个值的集合和定义在这个值集上的一组操作的总称”。从中可以看出,数据类型主要包括两个概念,一是有一个集合概念,即有这么一类数据,它们有某些共同的特性;二是同一种数据类型对应同一组可进行的操作。这两点到计算机系统中,就产生了程序员眼中的数据类型,即数据类型对应一片固定大小的内存,而且在这片内存上可以进行某些特定的操作。

之所以再三强调数据类型这个概念,是因为它是对一片固定内存的抽象,有了这个概念,程序员就可以抛开具体的硬件环境,通过变量来管理使用内存了。这些概念是联系底层和我们业务逻辑的一个关键,对于某些更高级语言程序员,可能不用再考虑这些问题了,但对于C的程序员,这些概念是必须掌握的。首先,系统为不同的数据类型定义了不同的内存大小,由于内存是以字节(8个二进制位),所以数据类型一般是以字节为单位的。比如每次C程序员都耳熟能详的int类型占4个字节,double类型占8个字节(32位机)等等。还有一个比较冷门的知识是端的概念,因为像int这样的数据类型,它是由4个字节的组合来表示一个数据的,而这些字节是有地址的,我们假设这4个字节在系统中连续存放(真是系统也是真么做的),那是该把数据权位高的放到高地址还是把权位低的放到高地址呢?比如一个int型数据的十六进制形式是0x11223344,对应的内存地址为0x....0010~0x....0013,是该把0x11放到0x....0010中呢(称为大端模式)还是把0x44放到0x....0010中呢(称为小端模式)?这两个选择并没有技术上的差别,无论哪种实现,技术实现都是一样的。而实际上这两种方法都是在真是系统中存在的,也就是有的硬件平台采用的是小端模式,而有的平台采用的是大端模式。

2. 整数表示及运算

整数是现实世界中使用最多的数,在计算机系统中,为整数定义了很多数据类型。最常用的就是int了,还有表示范围比较小的short,或者表示范围比较大的long long,另外还有有符号无符号的差别。这些数据类型都是表示整数的,但由于数学意义上的整数范围很广,所以为了方便使用(后面将会看到这些方便是以某些麻烦为代价的),计算机系统将他们抽象为不同的数据类型。由前面的分析可知,也就是他们占的内存大小不同或可进行的操作不同(理解数据类型一点要理解到这一点)。

既然都是整数,那不同的整数数据类型之间的相互转换就显得很自然。但这件自然的事对计算机系统或者说程序员来说却不没那么容易。原因很简单,就是因为不同数据类型占的内存大小不同,或者支持的操作不同。这就牵扯出两点:一是如果转换过程中有较大数据类型转换为较小的数据类型,那就有可能出现信息丢失的情况;二是如果把一个数据转换为另一个数据,那它是否支持新类型的操作呢?这两点其实隐含了由于整数表示给程序设计带来的两个重要问题:数据溢出和类型不匹配。

数据溢出的概念每程序员都能理解,因为用于存储数据的内存大小是有限的,如果一个数据太大或太小了,那就会出现溢出的情况。但理解了并不一定就能做到,尤其是在某些运算过程中,数据溢出是程序员经常犯的错误之一。要树立这个意识是一个漫长的过程,当然,这也从侧面说明了数据溢出并不是非常常见,或许只有当你经历过几次这样的错误才会对它引起足够的重视。

类型不匹配是一个更广泛的概念,它涉及到程序中所有的类型转换情况。在理想情况下,一种数据类型最好只和同种的数据类型进行运算,这样结果就可以获得最大程度上的确定性。但由于很多类型(这其中当然包括各种整数类型)之间有着天然的联系,比如说一个整数类型的5如果不能当做浮点类型的5.0来参加运算,就会显得很奇怪,程序编写就会很生硬。这种错误简直是太多了,这里举一个比较常见的例子就是有符号和无符号数之间的运算。比如我们要比较-3和5的大小,这个情况很常见,并不是什么冷门操作。如果这两个数的数据类型都是有符号的整数,那很简单,做一个(有符号)减法就可以了。但如果5是一个无符号数呢,该不该支持这种比较操作呢?该怎么支持呢?对于第一个问题,C语言为了保持最大的灵活性,是选择了支持的,但其支持方式却是隐式(会报一个警告)的将有符号数转换为无符号数。无疑,这种隐式转换在方便了程序员的同时也方便了bug,而且这种bug对于没有这方面意识的程序员简直就是一场噩梦。Java为了防止这种错误,取消了无符号数这种类型,拯救了一大批C++程序员。

(既然提到了Java,就多说几句,Java为了移植性,固定了每种数据类型的内存大小,而不是像C一样给出一些建议,而具体的大小是编译器设计者自己决定的,所以Java中也不再需要sizeof这种东西了。或许你还不知道,sizeof主要就是为了使代码兼容不同平台出现的。正如Bruce Eckel在其《Java编程思想》中提到的,C++程序员可能会变成最大的、热衷于Java的群体。C++作为曾经最流行的编程语言,现在正在逐渐演变一种情怀了)

很遗憾这里没有将到具体的整数编码方法,即参加运算时的各种情况。对这些细节感兴趣的同学可以买一本书看看。在这里值得一提的是作者对补码的解释,个人感觉要好过某些教材上的定义。

3. 浮点数

浮点数对于大多数程序员来说是一个黑洞,很少有人深入的去了解其底层实现原理。说实在的,本人也没有找到必须去探索它的理由。这里只给大家介绍两点,如果有专门研究这部分的,还是找本书自己啃一下。

首先是浮点数的不精确性,这第一点上面写提到了,虽然浮点数占用的内存比整数大很多,但其表示结果却是不精确的。这一点体现在程序中的科学计算上,我们必须对浮点数的计算结果进行某些调整才能得到正确的结果。尤其是一些分数之间的运算。或者一些判0的操作,比如矩阵中的化为对角阵过程,有些时候必须把某些足够小的数手动置为0才能得到一个对角阵。

其次是意识到不精确性的积累问题,由于现在计算机系统的计算精度都比较高了,所以一般情况下一两次精度的损失还不足以引发不良后果。但如果这个不精确性反复累计,那经过一段时间,就可能会出问题。这在一些特殊领域显得非常重要,比如军事航天类的控制系统中,一旦不精确性达到一定程度,就有可能引发灾难性后果。

将现实中的信息映射到计算机系统中的过程,就是要用二进制刻画整个大千世界。经过先辈的不懈努力,这个映射过程已经可以在大部分情况下正常进行了,但仍然有一些缺陷,这些缺陷导致程序设计者容易犯这样那样的错误。我们中的大多数人无法改进这些映射方法,我们要做的就是注意这些缺陷给我们的程序带来的错误风险。










评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值