其实,这个问题的实际意义是,既然说计算机内部的有符号整数都是补码,那么怎么才能有效的实现这一设计呢?潜台词是:根据上面由原码推导出补码的理论,如果是正数,计算机得到其原码,也就是得到了其补码(正数补码等同于原码),如果是负数,先得到其原码,然后再取反加一就可以了。也就是说按这个思维设计编译器,或计算机电路,就可以了。但是如果出现开始说的 -128的补码问题,这个设计就不能工作了。其实,在真正的设计中,这种获得负数补码的“取反加一”的方案根本没有实施过!
拿现代的计算机举例,输入设备只有键盘,通过编辑器程序把键盘的扫描码变成ascii码存储起来,比如输入 ‘a’就存储 0x61 ,输入 ‘5’就存储 0x35 ,这些都是ascii码。当你想得到真正的机器数时(注意,机器数和ascii是不同的,字符‘5’的ascii码是0x35,而数字5的机器数是0x5),需要借助编译器把表示数字的字符串从ascii码变成真正的机器数,比如你想得到 56的机器数(就是0x38),就可以输入语句“ db 56 ”,让汇编器程序帮你把56的ascii码字符串 :“0x35, 0x36”,转变成真正的机器数 0x38。这一转化需要这样一段程序:先把 ‘5’与 ‘0’做减法,就是 0x35 – 0x30 得到 0x5 (这一步就将ascii字符5变成了真正的机器数5),再把 0x5 与 0xa (就是十进制 10)相乘得到 0x32 (就是十进制 50) ,然后再把 ‘6’与 ‘0’做减法,就是 0x36 – 0x30 得到 0x6 (就是机器数6),最后把 0x32 与 0x6 相加,得到 0x38 ,就是机器数56了。
如果想得到 -56 ,就用语句 “ db -56 ”,这里得到机器数 56 的步骤与上面一致,只是最后要把 0xff 与 0x38 相乘,就是 -1 * 56 ,最后得到 0xc8 ,就是 -56 的补码。
这里我们看到,得到 -56 得补码时根本没用什么“取反加一”,这里处理的过程都是很自然的,只要考虑各个数值的运算,而不用考虑数值的补码形式,以及如何得到负数的补码。为什么?因为加法,有符号乘法等指令的电路,都是按补码输入,补码输出来设计的,你只要保证输入的是补码,输出的肯定也是补码。所以,只要你输入 -1 的补码 0xff ,与56的补码 0x38 ,得到的自然是 -56 的补码 0x c8 。综上,我们在获得 -56 的补码时,没有采取先得到 -56 的原码,然后除符号位外各位取反,最后再总体加一的方式。
由此可见,计算机是一个相当“封闭”的系统,他内部所有的有符号整数都是补码形式存在的,只要按数值的实际意义考虑问题,不用担心它的存储方式,比如想让 -56 与 6 相乘,你根本不用担心结果那个负数怎么变成补码,所有的运算电路都是按补码设计的。换句话说,“封闭”的计算机内部的有符号数都是补码的形式存在的,你根本不用考虑什么原码,什么取反加一了,你只要考虑你想要的数值就可以了,不要担心他怎么存储的。
有了上面那个ascii码到机器数的转换程序后,数字的输入就不再是问题了,但是,考虑的更远一点,如果在计算机的“蛮荒时代”,所有的指令,数据,都要用机器语言一位一位的输入时(搬开关、打孔),那时的有符号整数又怎么输入呢?确实,在那个环境下,就只能用我们的大脑计算了,把数字在大脑中转换成补码的样子,然后输入。其实,真正需要我们在大脑里转换然后再输入的数只有5个,分别是 ‘+’,‘-’,‘0’,0,-1 ,他们的补码分别是:0x2b ,0x2d,0x30,0x00,0xff ,好了,用这5个补码和机器指令编出我们刚才讲的ascii码转化机器数的程序,从此以后,我们只要输入数字的ascii字符串就可以了,让这个程序帮我们转换成补码,不用再辛苦的计算补码了。
深入到机器层,机器层面也根本用不着“原码取反加一”,因为机器里的所有数字,都是人工或者是编译器输入的,已经转换成了补码了,他本身已经是一个完备而封闭的系统了,根本没有接口接收其他有符整数的编码方案了。需要注意的是有一个取补指令neg ,这里的取补不是本来意义的“取反加一”(本来意义的“取反加一”只对负数),而是不论正负,把每一位取反,最后加一,就是说 neg 20 结果为 -20 ,neg -56 结果为56 ,就相当于把操作数与 -1 像乘了。这显然与为得到负数的补码采取的“取反加一”截然不同。当两个数做减法时,比如:有符号整数 60 (0x3c) 与 有符号整数 77 (0x4d)相减,加法器有一段电路把减数77取反加一,但是,请注意,这个电路跟neg一样,不管正负每位取反最后加一,就相当于用 乘法指令imul 把 操作数 与 -1 相乘了。这也跟求负数的补码采取的“取反加一”截然不同。
综上,无论在编译器层面还是机器层面,“负数的补码为其原码除符号位外取反然后总体加一”这个方法都没有用上,这只是教科书上提供的便于记忆的方法而已。
根据上面的说法,分析下c中具体的问题: c中int占4个字节,表示范围从 – 2147483648 到 2147483647 。
问题1:int -2147483648 ;编译后的值正确吗?
上面说的转换程序,这个编译的步骤为: 计算2*10^9 + 2*10^8+ …… +4*10+8 ,因为int只能表示到 2147483647 (0x7fff ffff),再加1的话就溢出了,得到了:0x8000 0000 ,(就是 -2147483648) ,然后再把这个结果与 -1 就是 0xffff ffff 相乘,得到的结果也溢出了,为:0x8000 0000 ,但是这恰好是 – 2147483648的补码,所以,虽然编译中虽然出现了两次溢出,但得到的结果是正确的。
问题2:int x = - 2147483648; 那么 –x 是 2147483648吗?
因为 – 2147483648 (0x8000 0000)与-1 (0xfff ffff)相乘结果为:0x8000 0000 就是– 2147483648 ,所以 –x 的结果还是 – 2147483648 而不是2147483648 。 所以,凡是结果可能超越表示范围的时候,一定要慎重,因为结果可能是你意料之外的。