前言
学习二进制及相关运算是计算机学习的重要部分。为了解决我在学习汇编时遇到的问题,我想在这篇博客中梳理并巩固之前所学习的二进制内容,从而更好的往下学习。
进制
进位制又称进制,是一种记数方式,亦称位置记法(positional notation)、数字命位法、定位记法、进位记数法、位值记数法(place-value notation)、位置数值系统(positional numeral system);利用这种“记数法”,可以使用有限种“数字符号”来表示所有的数值。
– wikipedia
我们日常生活中使用十进制进行计算,即为逢十进一,因为一个正常人的手指头为十根,方便我们计数。但是我们在学习其他进制时不需要割去自己的手指,或用上我们的脚趾。
古巴比伦人使用六十进制计算,时至今日仍用作记录 时间, 角度 等。
一进制以1为底数, 与其他进制不同,一进制只有数字 ‘1’, 无法恰当的表示数字0。
而计算机使用二进制, 以 0和1 表示, 方便电路信号模拟数字。
进制转换
我们计算时使用十进制,但计算机使用二进制。为了与计算机更好的交流,我们需要使用到进制转换:
N进制 -> 十进制:
譬如将二进制数转化为十进制数,将二进制数每一位乘上位权,再乘上这一位的数字,最后相加得到十进制数:
$$ \begin{align*} (1001)_2 &= 2^3 \times 1 + 2^2 \times 0 + 2^1 \times 0 + 2^0 \times 1 = (9)_{10} \\ \text{同样的:} \\ (567)_8 &= 8^2 \times 5 + 8^1 \times 6 + 8^0 \times 7 = (375)_{10} \end{align*} $$十进制->N进制:
将十进制数转换为其他进制数,很方便的一个方法是短除法。
例如将十进制转换为二进制:
1 | 185 / 2 = 92 ...... 1 |
将余数从下向上读,
从而我们得到了 $$(185)_{10} = (10111001)_2$$
同样的:
1 | 185 / 8 = 23 ...... 1 |
所以有$$(185)_{10} = (271)_8$$
数字的存储方式
我们使用十进制运算,而计算机使用二进制。所以我们需要将数字以二进制的形式存入计算机。
显然,对于一个正数或一个无符号数,我们只需要将其转化为二进制存入计算机即可
但对于一个负数,我们需要一些特殊的办法使其能够存入计算机,并且能够正常运算。
位(bit)
位是信息的最小单位,一位只能表示 0或1。
符号位和原码
为了表示一个二进制数的正负,我们令这个数字的第一位为符号位,0即为正,1即为负。符号位只表示这个数的正负,不改变它的绝对值。
例如,$$(-3)_{10} = (1000\ 0011)_2$$
这样我们可以用二进制表示一个负数。
同样:
这里的1000 0011和0000 0011都为原码,原码即未经更改的码。
反码
原码只能用来表示一个数,不能直接进行参与运算,原码的符号位必须与其他位分离开,这样做增加了硬件的开销和复杂性。
对于一个简单的 $1 + (-1) = 0$
当我们使用原码计算:$$(0000\ 0001)_2 + (0000\ 0001)_2 = (1000 0010)_2 = (-2)_{10} \neq 0$$
显然我们得到了一个错误答案。
为了进行正确的运算,反码应运而生,一个正二进制数的反码即为它本身,而对于一个负数,它的反码为将它除符号位以外所有的位取反得到的结果。
例如:
$$(0010\ 1001)_原 = (0010\ 1001)_反$$ $$(1010\ 1001)_原 = (1101\ 0110)_反$$对于运算$1 + (-2) = -1$ :
$$(0000\ 0001)_反 + (1111\ 1101)_反 = (1111\ 1110)_反 = (1000\ 0001)_原 = -1$$补码
运用反码,我们已经基本可以进行正确的运算了。但是对于之前提到的运算$1 + (-1) = 0$ :
$$(0000\ 0001)_补 + (1111\ 1111)_补 = (0000\ 0000)_补 = 0$$因为1111 1111的符号位为1,所以得到最终结果为-0,但我们又知道0的符号是没有意义的,所以为了解决0的两个编码的问题,我们引入了补码。
正数的补码是它本身,而负数的补码是它的反码 + 1。
例如:
$$(0000\ 1001)_原 = (0000\ 1001)_补 $$ $$(1000\ 1001)_原 = (1111\ 0110)_反 = (1111\ 0111)_补$$此时我们计算$1 + (-1) = 0$ :
$$(0000\ 0001)_补 + (1111\ 1111)_补 = (0000\ 0000)_补 = 0$$从而,我们在解决0的不同编码问题的同时,得到了正确答案。
同时,我们用1000 0000表示-128而非-0。
所以用补码存储数据时,一个八位数的数据范围为$[-128, 127]$,而当用原码或反码存储时,数据范围为$[-127, 127]$ 。
位运算
位运算对我们来说可能比较麻烦,但对计算机来说,一次位运算要进行的操作数远少于一次加法或其他运算。
与(and, &):
对两个二进制的每一位进行运算, 当两位数都为1时,运算结果为1, 否则为0。
$$(1011\ 0010)_2 \ \&\ (0010\ 0101)_2 = (0010\ 0000)_2$$或(or, |):
对两个二进制的每一位进行运算, 当两位数都为0时,运算结果为0,否则为1。
$$(1011\ 0010)_2 \ |\ (0010\ 0101)_2 = (1011\ 0111)_2$$亦或(xor, ^):
对两个二进制的每一位进行运算,当两位数不同时,运算结果为1,否则为0。
$$(1011\ 0010)_2 \wedge (0010\ 0101)_2 = (1001\ 0111)_2$$非(not,~):
对一个二进制数的每一位,将1变为0, 0变为1。
$$\sim(1011\ 0010)_2 = (0100\ 1101)_2$$左移(shl, <<):
将二进制数补码每一位左移一定位数,位移舍弃最高位,低位补0,对二进制数左移一次相当于将这个数乘2。
$$(1001\ 0010)_原 << \ 1 = (1110\ 1110)_补 << 1 = (1101\ 1100)_补 = (1010\ 0100)_原$$右移(shr, >>):
与左移相反,向右位移,舍弃低位,高位补符号位。对二进制数右移一次相当于将这个数除以2,向下取整
$$(1001\ 0010)_原 >> 2 = (1110\ 1110)_补 >> 2 = (1111\ 1011)_补 = (1000\ 0101)_原$$(ps:c++中整数除法也会自动取整,但 ‘ \ ‘为向零整除,右移运算为向下整除)