一、二进制补码(Two's Complement)
几乎所有机器都是用二进制补码来表示有符号整数。书中有公式来说明,比较复杂,这里就不写了。简单来说,用最高位表示符号,0为正,1为负;负数的值,等于相应的正数的按位取反后,加1。
例:+9的二进制补码表示为00001001,那么安位取反后为11110110,加1后为11110111,即-9
反之,一个负数对应的正数也可以表示取反加1
例:-9的二进制补码表示为11110111,取反后为00001000,加1得00001001
二、二进制反码(Ones' Complement)
二进制反码比较诡异,到目前为止我还不知道什么机器用反码表示负数。用最高位当作符号位,0为正,1为负。故而+9(00001001)的负数就表示为10001001,只将首位替换为1,其余位不变。
三、为什么用二进制补码而不用二进制反码
原因其实就是二进制反码不能一一对应,而二进制补码可以。比如,16的二进制表示为00010000,-8的二进制反码表示为10001000,而16+(-8)的值为10011000=-24,显然不对。所以如果用二进制反码表示负数的话,正数和负数的加法还得再定义一套规则,这就意味着需要用两套电路来实现加法。如果用二进制补码,-8表示为11111000,16+(-8)=100001000,最高一位溢出了,需要去掉,所以结果为00001000=8。
注意,二进制补码的英文和二进制反码的英文,‘ 的位置不一样。
为什么二进制补码可以精确的计算,这里有一篇文章,讲的很详细,也很优美,我上面的例子就是摘自此文章,有兴趣可以看一下
《关于二进制补码》
按位打印整数:
int show_bits( unsigned char *c, size_t len, char *str ) { #define BITS_PER_BYTE 8 #define BITS_MASK 0x80U //1000 0000 int ii; int jj; unsigned char *p = c; unsigned char temp; if( NULL == c ) { return -1; } if( NULL != str ) { printf("%s\r\n", str ); } /* 每个字节循环 */ for( ii = 0; ii < len; ii++ ) { /* 对字节中的每个比特循环 */ for( jj = 0; jj < BITS_PER_BYTE; jj++ ) { /* 只保留第jj 位,其余位置为0 */ temp = *p & ( BITS_MASK >> jj ); /* 将此位移到最低位,取此位的值 */ temp = temp >> ( BITS_PER_BYTE - 1 - jj ); printf("%d", temp ); } printf(" " ); p++; } printf("\r\n" ); return 0; }
char a = 123; char b = -123; show_bits( (unsigned char*)&a, sizeof(a), "a:"); show_bits( (unsigned char*)&b, sizeof(b), "b:"); printf("a = %c, a = %d, a = %u, a = 0x%x\r\n", a, a, a, a ); printf("b = %c, b = %d, b = %u, b = 0x%x\r\n", b, b, b, b );
10000101 —— -123的二进制表示,将123的二进制表示取反,得出10000100,再加1,为10000101
a = {, a = 123, a = 123, a = 0x7bb = ?, b = -123, b = 4294967173, b = 0xffffff85
上面为123和-123对应的字符、有符号整数、无符号整数、十六进制的表示
注意:
1. 上面的输出中,-123的%u打印为4294967173,这是因为由于%u将10000101当作无符号数,所以打印出对应的无符号整数4294967173。同理,%x的打印为0xffffff85
2. %d,%u,%x均为整数的打印,%u打印时,由于要将一个负的char,当作无符号整数打印出来,所以打印之前会进行扩展,从一个字节扩展成4个字节。那么前面的三个字节是按照0填充还是按照1来填充呢?关于二进制补码的扩展,一律按照符号位来填充。
01111011 填充之后:00000000 00000000 00000000 01111011 = 123
10000101 填充之后:11111111 11111111 11111111 10000101 = 4294967173
再看一段代码:
unsigned char a = 155; char b = 155; show_bits( (unsigned char*)&a, sizeof(a), "a:"); show_bits( (unsigned char*)&b, sizeof(b), "b:"); printf("a = %d, a = %u\r\n", a, a ); printf("b = %d, b = %u\r\n", b, b );输出:
a:10011011
b:10011011
a = 155, a = 155
b = -101, b = 4294967195
这里,b为-101。b的二进制表示为10011011 ,而有符号整数的范围,为-128-127,155已经超出了此范围,所以%d将10011011解释为一个有符号数,为-101。
从上面的输出中可以看出,虽然用%d打出的结果不同,但a和b在内存中的表示都是一样的,均为10011011。那么,能否直接用155与a和b进行判断呢?看下面的代码:
unsigned char a = 155; char b = 155; show_bits( (unsigned char*)&a, sizeof(a), "a:"); show_bits( (unsigned char*)&b, sizeof(b), "b:"); if( 155 == a ) printf("!!!a == 155\r\n"); if( 155 == b ) printf("!!!b == 155\r\n");
a:
10011011
b:
10011011
!!!a == 155
可以看出,if( 155 == b )这个条件根本没有进去。但b的值10011011 确实等于155啊,为什么会进不去呢?将可执行文件进行反汇编,得出的汇编代码如下:
if( 155 == a ) 804853e: 0f b6 44 24 1f movzbl 0x1f(%esp),%eax 8048543: 3c 9b cmp $0x9b,%al 8048545: 75 0c jne 8048553 <main+0x60> printf("!!!a == 155\r\n"); 8048547: c7 04 24 30 86 04 08 movl $0x8048630,(%esp) 804854e: e8 01 fe ff ff call 8048354 <puts@plt> if( 155 == b ) printf("!!!b == 155\r\n"); return 0; 8048553: b8 00 00 00 00 mov $0x0,%eax
可以看出,if( 155 == b )的这个条件判断,压根就没有被编译器生成汇编代码!也就是说,编译器认为b被定义为char类型,默认是有符号的,范围是-128-127,永远不可能等于155,所以干脆不将这条C语句翻译为汇编代码,也就是说这个条件永远进不来!我在工作中就遇到过此种情况,定义一个char类型的变量,然后对其赋值。如果有异常情况发生,将其赋为0xFF,然后再函数外面再做出错误处理,但由于编译器没有编译此条语句,所以永远不可能进行错误处理,这个问题当时百思不得其解,最后只得将类型改为unsigned char才解决
另外,此问题应该与编译器的优化细节相关,因为不可能所有编译器都会做这样的处理。我的编译器是gcc,从o1到o3,每个优化级别的汇编代码均没有这个判断。
万恶的是,编译器不认为这是一个bug,不会产生任何告警信息,只会默默的把我们写的错误代码优化掉,让我们陷入无穷的烦恼中。
更万恶的是,由于编译器对此条语句的处理是在优化时进行的,那么意味着前面的语法解析,词法解析等工作仍然会处理此语句,最初的汇编代码也还是会生成此
条语句的,只是在最后被干掉了。如果认为的在这个判断里加入一个错误,那么编译器是可以检查出来的,这反而更增加了我们排查的难度,所以以后遇到运算的操作,
首先应该想到的就是有没有溢出。
if( 155 == b ) { /* 我们看到的不是一个言行谨慎的大使,而是一个... */ add a error printf("!!!b == 155\r\n"); }