C语言的本质(3)——整数的本质与运算

 

计算机存储的最小单位是字节(Byte),一个字节通常是8个bit。C语言规定char型占一个字节的存储空间。如果这8个bit按无符号整数来解释,则取值范围是0~255,如果按有符号整数来解释,则取值范围是-128~127。C语言规定了signed和unsigned两个关键字,unsigned char型表示无符号数,signed char型表示有符号数。

 

对于char类型,编译器可以定义char型是无符号的,也可以定义char型是有符号的,在该编译器所对应的体系结构上哪种实现效率高就可以采用哪种实现,x86平台的gcc定义char是有符号的。

这是C标准的规则之一:优先考虑效率,而可移植性尚在其次。这就要求程序员非常清楚这些规则,如果你要写可移植的代码,就必须清楚哪些写法是不可移植的,应该避免使用。另一方面,写不可移植的代码有时候也是必要的,比如Linux内核代码使用了很多gcc特性以得到最佳的执行效率,在写的时候就没打算用别的编译器编译,也就没考虑可移植性的问题。如果要写不可移植的代码,你也必须清楚代码中的哪些部分是不可移植的,以及为什么要这样写,如果不是为了效率,一般来说就没有理由故意写不可移植的代码。

C语言与平台和编译器是密不可分的,离开了具体的平台和编译器讨论C语言,就只能讨论到本书第一部分的程度了。注意,ASCII码的取值范围是0~127,所以不管char型是有符号的还是无符号的,存一个ASCII码都没有问题,一般来说,如果用char型存ASCII码字符,就不必明确写signed还是unsigned,如果把char型当作8位的整数来用,为了可移植性就必须写明是signed还是unsigned。

 

ANSI C99标准中定义了两类(四个)类型修饰符:long/short和unsigned/signed。

  C99标准规定,long类型不能比变通类型短,short类型不能比普通类型长。而unsigned与signed的区别在实现上是有无符号的区别,而是使用上是取值范围的区别,两者表示范围相同,但前者全是正数,后者关于0对称。

 说明:

 

long/short可以修饰int,long还可以修饰double。

unsigned/signed可以修饰int, char,不可以修饰浮点型。

int长度是机器的字长,short int是半个字长,long int是一个或两个字长。

unsigned/signed长度与普通类型一样,只是表示区间不同。

 

各种整数数据类型的表示和取值范围:

整型

[signed]int

-2147483648~+2147483648

无符号整型

unsigned[int]

0~4294967295

短整型

short [int]

-32768~32768

无符号短整型

unsigned short[int]

0~65535

长整型

Long int

-2147483648~+2147483648

无符号长整型

unsigned [int]

0~4294967295

字符型

[signed] char

-128~+127

无符号字符型

unsigned char

0~255

 

我们已经了解了计算机中正整数如何表示,加法如何计算,那么负数如何表示呢?减法又如何计算呢?为了书写方便,下面所有的例子都用8个bit表示一个数,实际计算机算术运算的操作数可以是8位、16位、32位甚至64位的。

要用8个bit表示正数和负数,一种简单的思路是把最高位当作符号位(Sign Bit),0表示正1表示负,剩下的七位表示绝对值的大小。例如-1表示成10000001,+1表示成00000001。

计算机要对这样的两个数做加法运算需要处理以下逻辑:

1、如果两数符号位相同,就把它们的低7位相加,符号位不变。如果低7位相加时在最高位产生进位,则结果超出7位所能表示的数值范围,这称为溢出(Overflow),通常把计算机中的一个标志位置1表示产生溢出。

2、如果两数符号位不同,首先比较它们的低7位谁大,然后用大数减小数,结果的符号位和大数相同。

 

减法运算需要处理以下逻辑:

1、如果两数符号位相同,并且低7位是大数减小数,则符号位不变,如果低7位是小数减大数,则按大数减小数计算,结果要变号。

2、如果两数符号位不同,把低7位相加,如果是正数减负数则结果为正,如果是负数减正数则结果为负,低7位在相加时可能产生溢出。

 

这其实和手算加减法的逻辑是相同的。算加减法需要处理这么多逻辑:比较符号位,比较绝对值,加法改减法,减法改加法,小数减大数改成大数减小数……这是非常低效率的。还有一个缺点是0的表示不唯一,既可以表示成10000000也可以表示成00000000,进一步增加了逻辑的复杂性,所以我们迫切需要重新设计数的表示方法,以使计算过程更简单。

有一种方法可以把减法全部转化成加法来计算,这样就不必设计加法器和减法器两套电路了。我们以十进制减法为例来理解一下这种方法。比如:

 

167-52=167+(999-52)-1000+1=167+947-1000+1=1114-1000+1=114+1=115

 

首先把52换成999-52,也就是947,这称为取9的补码(9's Complement),虽然这也是减法但它不需要借位,只需要对每一位数字分别取补码,所以比一般的减法要简单得多。然后把167和947相加,百位上的进位舍去,得到114,然后再加1得到115[21],这就是最终结果了。一句话概括就是:减去一个数等于加上这个数取9的补码再加1(忽略最高位的进位)。

 

这种方法也可以类推到二进制加减法:减去一个数等于加上这个数取1的补码(1's Complement)再加1(忽略MSB的进位)。取1的补码就是1-1=0,1-0=1,其实相当于把每一位数字取反了,以后将1的补码简称为反码。比如

 

00001000-00000100->00001000+11111011+1->00000011+1=00000100

 

上式的前两步不是等价变换,所以没有用=号而是用->表示,第一步多加了一个100000000,第二步少加了一个100000000,效果相互抵消,所以最终结果正是00001000-00000100的结果。现在我们发现,如果把第一步写成00001000+(-00000100)->00001000+11111011+1,则11111011+1就可以用来表示负数-00000100。所以,补码表示法不仅可以把减法转化为加法,而且合理地规定了负数的表示方法,就是“先取反码再加1”。负数的这种表示称为2的补码(2'sComplement),以后简称为补码。为什么称为2的补码呢?因为如果对一位数取补码,则1的补码是1-1+1=10-1=1,相当于从2里面减去1。类似地,对00000100取补码是11111111-00000100+1=100000000-00000100,相当于从100000000(十进制的256)里面减去00000100。

 

将负数全部用补码表示之后,8个bit可以表示的正数有00000000~01111111(十进制的0~127),负数有10000000~11111111(十进制的-128~-1),合起来是十进制的-128~127,一共256个数,而8个bit最多可以表示28=256个不同的数,所以已经充分利用了这8个bit,每个数都只有一种表示,0也只有一种表示就是00000000。我们还发现,所有正数的最高位是0,所有负数的最高位是1,因此最高位仍然具有符号位的含义,要检查一个数是正是负只要看最高位就可以了,但在计算时却可以把符号位和数放在一起做加法运算,而不必像Sign and Magnitude表示法那样对符号位单独处理。

 

采用补码做加减运算时总是忽略MSB的进位,如果在计算过程中忽略进位的效果没有相互抵消,最后的结果肯定是错的,这种情况一定是由溢出引起的。只要我们有办法判断哪些情况会产生溢出,其它情况下都可以放心地忽略MSB的进位。判断溢出的办法是这样的:在相加过程中最高位产生的进位和次高位产生的进位如果相同则没有溢出,否则就说明产生了溢出。逻辑电路的实现可以把这两个进位连接到一个异或门,把异或门的输出连接到溢出标志位。对于8位二进制数的加减运算来说,当计算结果超出-128~127的范围时就会溢出,例如:

有符号数加法溢出

最高位产生的进位是1,次高位产生的进位是0,说明溢出了,计算结果换算成十进制是122,这显然不对,根本原因是(-126)+(-8)=-134超出了8位二进制数能表示的范围。

用8个bit既表示正数又表示负数,则能够表示的范围是-128~127,如果8个bit全部表示正数,则能够表示的范围是0~255,前者称为有符号数(Signed Number),后者称为无符号数。但是计算机在做加法时并不区分操作数是有符号数还是无符号数,计算过程都是一样的,所以上面的例子也可以看作无符号数的加法:

无符号数加法进位

 

把两个操作数看作无符号数分别是130和248,计算结果换算成十进制是122,最高位的一个进位相当于256,122+256这个结果是对的。计算机的加法器在做完计算之后,根据最高位产生的进位设置进位标志,同时根据最高位和次高位产生的进位的异或设置溢出标志。至于这个加法到底是有符号数加法还是无符号数加法则取决于程序怎么理解了,如果程序把它理解成有符号数加法,就去检查溢出标志,如果程序把它理解成无符号数加法,就去检查进位标志。通常计算机在做算术运算之后还可能设置另外两个标志,如果结果为零则设置零标志,如果结果的最高位是1则设置负数标志(只有当理解成有符号数运算时才去检查这个标志)。