整数的表示
一、二进制补码(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 );
输出的结果: a:
01111011 ——123的二进制表示
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");
}
test.c: In function 'main':
test.c:60: error: 'add' undeclared (first use in this function)
test.c:60: error: (Each undeclared identifier is reported only once
test.c:60: error: for each function it appears in.)
test.c:60: error: expected ';' before 'a'
本文深入探讨了二进制补码与反码的概念及其应用,通过实例展示了这两种表示方法的区别,并讨论了为何现代计算机系统普遍采用补码而非反码。此外,还涉及了补码的扩展及对程序代码的影响。
1499

被折叠的 条评论
为什么被折叠?



