有符号数和无符号数

在计算机中,数值类型分为整数型或实数型,其中整型又分为无符类型或有符类型,而实型则只有符类型。 字符类型也分为有符和无符类型。在程序中,用户可以自己定义是否需要一个非负整数;

一、无符号数和有符号数的表示方式

以一个字节(char类型)为例:若想要表示正负号,一般需要一个位来标记,如取最高代表正负号,则有符号和无符号的数值最大值对比如下:

1 有符号:0111 1111 = 2^6+2^5+2^4+2^3+2^2+2^1+2^0     = 127; ==> 范围是 -128 ~ 127
2 
3 无符号:1111 1111 = 2^7+2^6+2^5+2^4+2^3+2^2+2^1+2^0 = 255;==> 范围是   0  ~ 255

由上可看出:

  1. 同样一个字节大小,有符号和无符号表示的范围不同,但个数相同均为256个;
  2. 单纯这样存储是存在问题:
    1. 针对有符号数,0在内存中存在两种方式即+0和-0;
    2. 针对负数的大小,-1(1000 0001)和-2(1000 0010)单纯的从二进制存储来比较,应该是-2(1000 0010) > -1(1000 0001)这与实际逻辑不吻合;

二进制补码避免了这个问题,这也是当今最常用的系统存储方式:即高位段0代表正数,1代表负数,表示正数为原码,而表示负数的方式采用:补码 = 反码+1

PS:

原码:一个整数,按照绝对值大小转换成的二进制数,最高为为符号位,称为原码。

反码: 将二进制除符号位数按位取反,所得的新二进制数称为原二进制数的反码。 正数的反码为原码,负数的反码是原码符号位外按位取反。取反操作指:原为1,得0;原为0,得1。(1变0; 0变1)

补码: 反码加1称为补码。

1 例如针对有符号数的±1内存存储形式为:
2 +10000 0001(原码)     ==> 0000 0001
3 -11111 1110(反码) + 1 ==>  1111 1111
4 -21111 1101(反码) + 1 ==>  1111 1110
5 这样做也符合了正常逻辑:-1 > -2....

注意: 单纯从一个字节8位二进制存储上来看,1111 1111 既可以表示有符号的-1又可以表示无符号的255

 1 //最高位是否表示正负号示例
 2 //0x80 = 1000 0000 &按位与操作,按位比较两数字,相同为1,不同为0;
 3 #include <stdio.h>
 4 int main()
 5 {
 6     char c = 1;
 7     short s = -2;
 8     int i = 3;
 9     printf("%d\n", ( (c & 0x80) != 0) );           //0
10     printf("%d\n", ( (s & 0x8000) != 0) );         //1
11     printf("%d\n", ( (c & 0x80000000) != 0) );     //0
12     return 0;
13 }

二、迷惑人的有符号下无符号数的比较操作

无符号数与有符号数间的比较

 1 #include <stdio.h>   
 2 int main() 
 3 {
 4     int a = -1; 
 5     unsigned int b = 1; 
 6     if(a > b) 
 7         printf("a > b, a = %d, b = %u\n", a, b); 
 8     else 
 9         printf("a <= b, a = %d, b = %u\n", a, b); 
10     return 0;
11 } 
12 //print: a > b, a = -1, b = 1

当执行一个运算时(如这里的a>b),如果它的一个运算数是有符号的而另一个数是无符号的,那么C语言会隐式地将有符号 参数强制类型为无符号数,并假设这两个数都是非负的,来执行这个运算。这种方法对于标准的算术运算(四则运算)来说并无多大差异,但是对于像<和>这样的比较运算就可能产生非直观的结果。

对大多数C语言的实现,处理同样字长的有符号数和无符号数之间的相互转换的一般规则是:数值可能会改变,但是位模式不变。也就是说,将unsigned int强制类型转换成int,或将int转换成unsigned int底层的位表示保持不变。

示例中

-1(变量a的值:1111 1111 1111 1111 1111 1111 1111 1111)这个有符号数强制转换成无符号数(1111 1111 1111 1111 1111 1111 1111 1111= 2^32-1= 4294967295,从二进制存储上来看,无符号数所有位都为1时表示的时最大值)然后再与 1(变量b的值:0000 0000 0000 0000 0000 0000 0000 0001)来进行比较;

总结:当有符号数遇见无符号数参与计算时,则有符号数进行转换为无符号数

三、查看验证结果(显示存储的形式)

为了证明上面所说的内容,写段代码将内存中的存储形式显示出来:代码中函数show_byte,它可以把从指针start开始的len个字节的值以16进制数的形式打印出来。

 1 #include <stdio.h> 
 2 void show_byte(unsigned char *start, int len) 
 3 {
 4     int i = 0; 
 5     for(; i < len; ++i) 
 6         printf(" %.2x", start[i]); 
 7     printf("\n"); 
 8 } 
 9 
10 int main() 
11 {
12     int a = -1;
13     unsigned int b = 4294967295; 
14     printf("a = %d, a = %u\n", a, a); 
15     printf("b = %d, b = %u\n", b, b); 
16     show_byte((unsigned char*)&a, sizeof(int)); 
17     show_byte((unsigned char*)&b, sizeof(unsigned int)); 
18     return 0; 
19 }
20 /*print:
21 a = -1, a = 4294967295
22 b = -1, b = 4294967295
23  ff ff ff ff
24  ff ff ff ff
25 printf函数中,%u表示以无符号数十进制的形式输出,%d表示以有符号十进制的形式输出。通过show_byte函数,我们可以看到,-1与4 294 967 295的底层表示是一样的,它们的位全部都是全1,即每个字节表示为ff。
26 */

四、由无符号数值参与减法运算的错误

1 //求某个数组中前length个元素的和的代码段:
2 int sum_elements(float a[], unsigned length) 
3 {
4     int i = 0; 
5     int sum = 0; 
6     for(i = 0; i <= length -1; ++i) 
7         sum += a[i]; 
8     return sum; 
9 }

因为数据的长度(或个数)肯定是一个非负数,所以把length声明为一个unsigned很合理,计算的数据个数和返回类型也正确。的确如此,但是这都是在length不为0的情况,试想,当调用函数时,把0作为参数传递给length会发生什么事情?回想一下前面我们所说的知识,因为length是unsigned类型,所以所有的运算都被隐式地被强制转换为unsigned类型,所以length-1(即0-1 = -1),-1对应的无符号类型的值为最大值,所以for循环将会循环(4 294 967 295)次,另一方面,当0-1操作结束时数组也会越界,发生错误。

那么如何优化上面的代码呢?其实答案非常简单,你也可以自己想一想,这里就给出答案吧,就是把for循环改为:for(i = 0; i < length; ++i) 

1 //比较两个字符串长度:判断第一个字符串是否长于第二个字符串,若是,返回1,若否返回0;
2 int strlonger(char *s1, char *s2) 
3 {
4     return strlen(s1) - strlen(s2) > 0; 
5 } 

在Linux下可用man 3 strlen命令查看,strlen函数的原型为:

size_t strlen(const char *s); 

该函数原型返回一个数据类型size_t,它被定义在stdio.h文件中,是一种机器相关的无符号类型,他被设计得足够大以便能够表示内存中任意对象的大小,即本质上属于unsigned int,

另一方面,一个字符串的长度当然不可能为负,这样的定义显然是合理的,但是有时却因为这样,而存在不少的问题,如函数strlonger的实现。当s1的长度大于等于s2时,这个函数并没有什么问题,但是你可以想像,当s1的长度小于s2的长度时,这个函数会返回什么吗?没错,因为此时strlen(s1) - strlen(s2)为负(从数学的角度来解释的话),而又由于程序把它作为unsigned为处理,则此时的值肯定是一个比0大的值,即永远为真,返回1。换句话来说,这个函数只有在strlen(s1) == strlen(s2)时返回假,其他情况都返回真。

 1 //测试无符号数减法
 2 #include <stdio.h> 
 3 #include <string.h> 
 4 
 5 int strlonger(char *s1, char *s2) 
 6 {
 7     return strlen(s1) - strlen(s2) > 0; 
 8 } 
 9 
10 int main() 
11 {
12     char s1[] = "abc"; 
13     char s2[] = "cd"; 
14     if(strlonger(s1, s2)) 
15         printf("s1 is longer than s2, s1 = %s, s2 = %s\n", s1, s2); 
16     else 
17         printf("s1 is shorter than s2, s1 = %s, s2 = %s\n", s1, s2); 
18 
19     if(strlonger(s2, s1)) 
20         printf("s2 is longer than s1, s2 = %s, s1 = %s\n", s2, s1); 
21     else 
22         printf("s2 is shorter than s1, s2 = %s, s1 = %s\n", s2, s1); 
23     return 024 }

若符合正常逻辑则修改strlonger函数改为:

1 int strlonger(char *s1, char *s2) 
2 {
3     return strlen(s1) > strlen(s2); 
4 }

这样就可以利用两个无符号数进行直接的比较,而不会因为减法而出现负数(数学上来说)而影响比较结果。

 总结:当无符号数参与计算时,尽量避免减法,代之为比较逻辑

五、无符号使用的建议

  1.  除一定非使用无符号类型时,才进行使用unsigned类型;
  2.  使用unsigned类型时避免比较运算和减法运算操作;

六、补充部分:整数的溢出

从第二项:迷惑人的有符号下无符号数的比较操作的分析部分可以看出:对于一个固定长度的无符号数:

1 MAX_VALUE +1 == MIN_VALUE
2 MIN_VALUE –1 == MAX_VALUE

可以把无符号数看作是汽车的里程表,当达到它能表示的最大值时,会重新从起始点开始;即:

  1. 对于无符号数(unsigned int)超过最大值时,变量会从0开始;
  2. 对于有符号数(               int)超过最大值时,变量会从-2147483648开始;

注意:溢出的行为是未定义的行为,也就是说,在程序运行时,溢出可能会从起始点开始,也可能是其它的情况;

posted @ 2018-07-07 08:02  芳舒晴  阅读(12088)  评论(0编辑  收藏  举报