有符号数和无符号数
在计算机中,数值类型分为整数型或实数型,其中整型又分为无符类型或有符类型,而实型则只有符类型。 字符类型也分为有符和无符类型。在程序中,用户可以自己定义是否需要一个非负整数;
一、无符号数和有符号数的表示方式
以一个字节(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
由上可看出:
- 同样一个字节大小,有符号和无符号表示的范围不同,但个数相同均为256个;
- 单纯这样存储是存在问题:
- 针对有符号数,0在内存中存在两种方式即+0和-0;
- 针对负数的大小,-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 +1:0000 0001(原码) ==> 0000 0001 3 -1:1111 1110(反码) + 1 ==> 1111 1111 4 -2:1111 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 0; 24 }
若符合正常逻辑则修改strlonger函数改为:
1 int strlonger(char *s1, char *s2) 2 { 3 return strlen(s1) > strlen(s2); 4 }
这样就可以利用两个无符号数进行直接的比较,而不会因为减法而出现负数(数学上来说)而影响比较结果。
总结:当无符号数参与计算时,尽量避免减法,代之为比较逻辑
五、无符号使用的建议
- 除一定非使用无符号类型时,才进行使用unsigned类型;
- 使用unsigned类型时避免比较运算和减法运算操作;
六、补充部分:整数的溢出
从第二项:迷惑人的有符号下无符号数的比较操作的分析部分可以看出:对于一个固定长度的无符号数:
1 MAX_VALUE +1 == MIN_VALUE 2 MIN_VALUE –1 == MAX_VALUE
可以把无符号数看作是汽车的里程表,当达到它能表示的最大值时,会重新从起始点开始;即:
- 对于无符号数(unsigned int)超过最大值时,变量会从0开始;
- 对于有符号数( int)超过最大值时,变量会从-2147483648开始;
注意:溢出的行为是未定义的行为,也就是说,在程序运行时,溢出可能会从起始点开始,也可能是其它的情况;