信息的编码方式
在计算机中信息都是以0、1两种数据来表示的,大家都知道,但是就是这两个简单的0、1如何实现了计算机的强大计算呢?这就涉及到了计算机中信息的表达和处理。在大学计算基础课上,开始就涉及了二进制、八进制、十进制、十六进制等进制之间的转换方式。其中二进制是计算机的实现方式,其他的进制都是出于一些目的定义的。在二进制中有三个基础概念:原码、反码、补码是非常重要的,很多人也对三者之间的转换方式非常清楚。
原码:对于十进制的数据,可以采用多个0、1构成的比特向量表示,其中最高位表示符号位,当为1时,表示这个数为负数,当为0时,表示这个数为正数。
反码:是指原码符号位除外的其他位进行取反操作,但是取反操作只针对负数,也就是说正数的原码等于反码。
补码:也只是针对负数而言,对于负数补码是指在反码的基础上加1即表示补码(但是不能改变符号位数据)。对于正数而言,补码等于反码,等于原码。
我们可以这样认为,为了解决负数的问题,在计算机中引入了反码、补码的概念,且补码、反码主要针对的对象就是负数,正数的补码、原码、反码是相同的bit向量。
在实际运用中补码相比另外两种编码方式更加的方便,因此对于有符号整形类型,几乎所有的编译器中都是采用补码方式进行编码,了解到编译器是采用补码的形式存储计算机的整形数据信息是非常重要的。
一般而言,对于有符号的整数型数据类型,即char、short、int这种类数据类型,都是采用补码方式编码的,但是对于无符号的整数型数据类型,一般都是采用原码的形式编码的,也就是单纯的比特向量,没有符号位之说,由于常用的计算机系统都是32bit,这也是为什么说计算机的地址空间是4G=2^32字节。char型数据类型占1字节的空间,而short类型一般占用2字节的空间,int型数据占用4个字节的空间。32个bit刚好占有4字节,因此我们可以认为int数据实际上就是一个32个bit向量。
由实际情况可知:
int型的范围是-2^31---2^31-1,可以认为是非对称的空间。无符号的unsigned int的范围则是0到2^32-1之间。
char型的范围是-128到-127之间,unsigned char的范围则是0到255之间,在很多的应用中可以充分运用char的范围减小存储量。
short的范围是-2^15到2^15-1,unsigned short 的范围是0-2^16-1。
需要了解的是有符号的整数型数据都是采用补码方式进行编码的,而无符号的数据类型一般采用原码方式编码(正数)。两种编码方式表示数据的范围存在差别,实际上在C语言编程的过程中都会进行隐式的强制类型转换,如果不清楚编码方式的差别,就很难准确的把握计算的差别。在嵌入式编程中经常会有一些简单的延迟操作,如果编写不恰当就会导致错误产生。如下所示:
void delay(int time)
{
unsigned char i = 0;
for(; time >= 0; -- time)
for(i = 0; i < 256; ++ i)
;
}
这个题乍一看没什么问题,但是仔细推敲就会发现存在问题,因为存在死循环,unsigned char的最大值是255,不可能大于等于256,因此一直满足条件,也就是说第二个循环会一直执行,这就是典型的不注意范围问题。
还有一个典型的排序问题:
unsigned char array[1000][1000];
void sort(unsigned char (*a)[1000])
{}
这种属于典型的大数排序问题,只有选择合适的排序策略才能减少排序的时间复杂度,那么如何实现呢?最简单的方式是采用计数排序,时间复杂度为O(n)。充分利用了unsigned char的数值范围在0-255之间这个范围。
左移右移处理
在整形数据类型中有一个问题就是典型的移位操作,在机器语言中也会有位操作,在C语言中也存在位操作,这为直接控制CPU提供了较好的实现方式。移位主要包含左移和右移操作,其中左移的实现是在当前数据的bit向量向左移动n个bit,后续的bit补0。
而右移比左移复杂一些,右移存在两种:逻辑右移和算术右移,逻辑右移主要是针对无符号型数据,逻辑右移是指将当前的bit向量向右移动,左边bits补零。算术右移主要针对有符号型数据,将当前数据右移,左边补上最高bit的值。这种实现方式能够保证数据的符号不改变,也就是说负数通过右移以后还是负数,不会变成正数。而逻辑右移则不行,负数通过逻辑右移以后就变成了正数,这是不合理的。因此可以总结如下:
对于左移操作,不管是有符号还是无符号类型,都是右端直接补零。这时候可能导致数据发生较大的变化,正数通过左移变成了负数,出现这些的原因实际上是左移操作忽略了进位操作,正负都是合理的。在整形数据中经常采用左移操作实现数据的乘法操作,两个正数相乘是有可能产生负数的。比如:
int x1 = 0x60000000;
printf("%d,%d,%d\n",x1,x1<<1,x1*2);
这段代码就说明了两个正数相乘是可以产生一个负数的,但是如果我们采用无符号数据类型进行左移操作就不会产生两个正数相乘产生负数的情况,这样能够避免很多不可思议的结果。
右移操作中的逻辑右移主要是针对无符号数据类型,对有符号的数据类型是无效的,对于有符号的数据类型,右移操作对于大多数的编译器都认为是算术右移,算术右移能够保证数据的正负特性,不会发生左移中两个正数相乘得到负值的情况。算术右移的实质是在移位操作以后,数据的左边采用符号位填充。通常在C语言中右移实现除法操作,比如8>>1,即实现了除以2的操作,对于无符号型数据可以采用右移操作实现除法操作,但是对于有符号数据类型可能出现错误。下面说一个典型的例子:
int x1 = -1;
printf("%d,%d,%d\n",x1,x1>>1,x1/2);
这个例子说明了有符号数据类型通过算术右移并不能完成除法操作,但是无符号的数据类型基本上可以完成除法操作。搞清楚何时是算术右移何时是逻辑右移是非常有必要的。
对于无符号数据类型,采用左移右移的方式提高乘除法的速度是可行的,但是对有符号的数据类型最好不要采用这种实现方式,因为可能会出现意想不到的结果。虽然很多的程序书中建议少用unsigned的类型,但是在移位操作这方面最好还是处理无符号型对象比较有保证。
强制类型转换处理
强制类型转换是C语言中比较重要的一个主题,其中程序员老手也会忽略这种问题的发生,基本的实现方式是有符号向无符号转变,小字节数据朝大字节数据转换,当然也有高字节数据类型往低字节数据转变的问题。
基本的转换存在两种:零扩展和符号扩展。
零扩展是针对无符号数据类型,将一个小字节数据转换为大字节数据时,在高字节中填充零,实现数据的一致型。
符号扩展则针对有符号数据类型,将一个小字节数据转换为大字节数据时,在高字节中填充小字节数据的符号位,填充符号位主要是为了实现补码一致原则。零扩展实际上是符号扩展的子类。
大字节数据向小字节数据转换的过程是一个截取过程,这样可能会导致数据正负的变化,也就是说大到小的过程可能发现比较大的变化,截取数据的一部分作为小字节数据的bit向量。
强制类型转换实质上包含了符号扩展,和零扩展,符号扩展保证了补码的一致型,零扩展则保证了数值大小的一致性,符号扩展针对有符号数据类型,而零扩展主要针对无符号数据类型。
很多时候的强制类型转换是隐含进行的,这是就需要我们准确的把握,有符号类型到无符号类型的转变会导致很大的差别,因为无符号类型数加上有符号类型数将进行有符号到无符号数据类型的转换,这时候就会产生一个无符号的数据,不会产生有符号类型的数。
同时还有一个技巧,在C语言中如何判断两个数的和是否越界的问题,由于很多编译器对越界并不报错,这时候可以通过判断两个数的和是否小于任何一个数即可,如果小于任何一个,则说明这个数就是越界,否则没有发生越界问题。
具体的实现可以采用下面的代码测试:
int test()
{
char x2 = -20;
int x1 = x2;
printf("x2 = %d, x1 = %u\n",x2,x1);
x2 = 20;
x1 = x2;
printf("x2 = %d, x1 = %u\n",x2,x1);
}
浮点型数据类型
浮点型数据类型在前一章中已经说明了,浮点型数据类型没有无符号和有符号之说。浮点型具有固定的编码方式,本来就是存在正负之分,即没有unsigned float之说。需要注意的是浮点型是一个近似的值不能用来比较。还有对float类型数据不能进行移位操作,因为float是有固定的编码方式的不像整形数据。
总结
了解计算机中的有符号整型数据一般按照补码的方式存在,有符号数据类型的扩展是按照符号扩展,而不是简单的零扩展,符号扩展是零扩展的延伸,主要是保证在延伸的过程中符号、数值保持不变。
在移位操作过程中,有符号、无符号数据类型的左移都没有问题,但是可能会导致数据类型的改变,特别是有符号数据类型。对于对于右移操作需要注意,有符号数据类型是算术右移,而无符号数据类型是逻辑右移,右移的差别是在左端补齐的值的差别。
在用移位来模拟数据的乘除法时特别要注意,对无符号数据类型是最有效的,左移右移的值也是我们认为正确的值,但是如果是对有符号数据类型进行右移模拟除法过程,左移模拟乘法过程时可能导致结果不是所想。因此建议只对无符号数据类型采用移位来简化乘除操作。