什么是二进制

  在了解二进制前我们要了解“进制”这个概念。进制是进位计数制的一个概念,“N进制”即代表遇到N就向前进一位。我们日常中使用的数字通常是十进制数,即逢十进一。通过这个概念以此类推,我们便能得到二进制、三进制……直到N进制。所以说不同进制之间的区别便是一位数上的最大值不同,是对同一个数字的不同表示方式,并且进制并不只有2、8、10、16四种,只是这四种比较常用而已。

  而二进制便是现代计算机中普遍应用的一种进制,原因便是二进制每一位上只有两个数:0或1,这使得二进制数在物理层面上更容易表达以及运算,也为电路中的逻辑运算提供了便利。

进制的转换

  初中的时候我们便学习过二进制与十进制的互换。

  十进制转换为二进制的方法称为除2取余法,即每次将整数部分除以2,余数为该位权上的数,而商继续除以2,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数读起,一直到最前面的一个余数。

  如图所示,十进制的500转换为二进制后就变为了111110100(2)。十进制转换为其他进制将除数2换成其他进制的值即可。

十进制转二进制图解

  二进制转换为十进制则是二进制数从低位到高位(即从右往左)计算,第0位的权值是2的0次方,第1位的权值是2的1次方,第2位的权值是2的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。

二进制转十进制

  其他进制转换为十进制的方法与二进制转十进制类似,只不过将2^n*m中的2换成对应的进制值就可以了。

二进制代码转换的奇技淫巧

  话不多说,直接上代码:

1
2
3
4
5
6
7
8
9
10
void convert(int n, char str[]) {
int index = 0;
for (int i = 31; i != -1; --i) {
int bit = (n >> i) & 1;
if (index != 0) str[index++] = (char) (bit + '0');
else if (bit != 0) str[index++] = '1';
}
if (index == 0) str[index++] = '0';
str[index] = '\0';
}

  这个代码使用位运算的方式将十进制数转换为二进制并存储在字符串中,该方法相比除二取余法更为简单,处理负数也不需要考虑补码的问题。至于性能问题并没有进行测试,有兴趣的小伙伴可以自行测试一下。

  代码的精髓主要在第四行,这句话的原理在了解了位运算后很容易就能想清楚了,这个代码留在这让各位自行品读,就不进行讲解了。

不同进制在代码中的表示方法

1
2
3
4
二进制:0b*****   (例如:0b11001001 -> 201(10))
八进制:0****** (例如:0757 -> 495(10))
十进制:******* (例如:551)
十六进制:0x*** (例如:0xaf58 -> 44888(10))

数字运算

  所有进制的运算依然符合小学所学的十进制运算方法,直接套用即可。

1
2
3
4
5
6
7
8
数字:         111101
数字: × 110
-------------------------
运算: 000000
运算: 111101
运算: 111101
-------------------------
求解: 101101110

数字的二进制表示

表示规则

  每一个二进制数被称为一位,这个数字有几个二进制数就占用几位。同时二进制数的最左端为最高位,最右端为最低位,即从右往左数,第一个数下标最小(0)。

  二进制中正负数是怎么区分的呢?重点就在最高位上,当最高位为 0 时表示这是一个正数,当最高位为 1 时表示这是一个负数。当然也有例外,就是unsigned ?型的数据,这种数据不把最高位当作正负号的判定,而是将其也用来存储数字,这样做将数据表示正数的范围扩大了一倍,但同时也丧失了表示负数的能力。

原码

  顾名思义,原码就是数字原本的码值,任何数的原码就是其本身。

反码

  反码比原码复杂一点,正数的反码是它本身,而负数的反码则是将除最高位的数外其它所有数反转,即 0 变成 1,1 变成 0。比如:0b10111的反码便是0b11000

补码

  正数依旧延续传统,其补码仍是其本身。

负数的补码

  负数的补码有很多种求法,这里列出两种:

  1. 尾数的第一个 1 及其右边的 0 保持不变,左边的各位按位取反,符号位不变。例如:0b111010变成反码就是从右往左数第一个 1 以及其右边的0和符号位保持不变,其它位单独求反,就变成了0b100110
  2. 将负数的反码 + 1。例如:0b111010的反码是0b100101,加一就变成了0b100110

  为什么这里要列出两个方法呢?因为第二个方法在网上广为流传,甚至被误认为其就是补码的定义,其实这只不过是补码凑巧等于反码+1而非因为反码+1是补码。如果你有兴趣了解这方面的知识,可以阅读《计算机组成原理》,其中更为详细且严谨的方式解释补码。

反码与补码的意义

  很多人可能会疑惑:为什么要创造补码这个概念?补码存在的意义是什么?为什么不直接用原码存储负数?

  现在假设只存在原码,很好,现在的概念非常简单,我们来做数学运算吧。

1
2
3
4
5
6
0101 + 0010 + 0101 = 1100 -> (5 + 2 + 5 = 12) //没问题
0000 + 1000 = 1000 -> (0 + (-0) = -0) //也没啥问题
0011 - 0010 = 0001 -> (3 - 2 = 1) //完全正确
1001 + 1001 = 0010 -> ((-1) + (-1) = 2) //啊?
0010 + 1010 = 1100 -> (2 + (-2) = -4) //?????
0011 + 1100 = 1111 -> (3 + (-4) = -7) //wtf?

  最后几个等式仿佛在逗我们玩一样,甚至我们都不能称之为等式,因为它左右的式子根本就不相等。仔细观察我们会发现,正数运算都很正常,但是一旦牵扯到负数就会出现各式各样的问题。所以原码,虽然直观易懂,易于正值转换。但用来实现加减法的话,运算规则总归是太复杂。于是反码来了。

  反码解决的问题是原码相反数相加不等于 0 的问题,现在让我们使用反码来进行一波计算。

1
2
3
4
5
6
0101 + 0010 + 0101 = 1100 -> (5 + 2 + 5 = 12) //没问题
0000 + 1111 = 1111 -> (0 + (-0) = -0) //也没啥问题
0011 - 0010 = 0001 -> (3 - 2 = 1) //完全正确
1110 + 1110 = 1100 -> ((-1) + (-1) = -3) //啊?
0010 + 1101 = 1111 -> (2 + (-2) = -0) //可以接受
0011 + 1011 = 1110 -> (3 + (-4) = -1) //正确

  正负数的加法问题解决了,但是负数与负数的运算依然是错误的。但是实际上,两个负数相加出错其实问题不大。我们的初衷是解决正与负的加法问题,虽然现在负负相加是错误的,但是正负数的差别只有符号位不同,如果想要运算负负相加只需要把两个负数转换为正数运算,再把符号位变成1就可以了。

  到这里,我们已经解决了数字运算的问题,但是依然存在一个小问题,就是0010 + 1101 = 1111,为什么2 + (-2)等于-0而不是0呢?虽然+00都一样,但是在小的问题也是问题,让我们来尝试解决它。

  解决思路很简单,把负数的反码 + 1,这样就不存在-0了,而数字的表达范围也从[-2^(n-1) + 1, 2^(n-1) - 1]拓展到了[-2^(n-1), 2^(n-1) - 1]注意:这里的解决思路是从反码与补码这个巧合的关系推出来的,不是说补码就是由反码 + 1 推出来的。

补码的优点

  补码的存在,可以将符号位和数值域统一处理,同时让计算机可以使用加法运算来解决减法问题,这样硬件层面只需要有加法器就可以了,而不需要添加减法器,简化了电路设计。

位运算

和(二元)[ & ]

  和运算规则中,同为1返回1,否则返回0。例如:

1
2
3
4
    0b1101
& 0b1010
-------------
0b1000

或(二元)[ | ]

  或运算规则中,同为0返回0,否则返回1。例如:

1
2
3
4
    0b1101
| 0b1010
-------------
0b1111

非(一元)[ ~ ]

  非运算规则中,0110。例如:

1
2
3
 ~  0b1010
-------------
0b0101

  这里注意,布尔类型和数字类型的非运算符号不一样,布尔类型是!,数字类型是~

异或(二元)[ ^ ]

  异或运算规则中,两数相同返回0,否则返回1,简而言之就是不进位的加法(或者是按位的不等于)。例如:

1
2
3
4
    0b1010
^ 0b1100
-------------
0b0110

左移(二元)[ << ]

  左移运算是将二进制数所有位向左移动指定位数,空位补0。例如:0b1111 << 2 = 0b[11]1100(括号中的数是被裁掉的数)。

1
2
3
4
    0b1111
<< 2
-------------
0b1100

右移(二元)[ >> ]

  右移运算是将二进制数所有位向右移动指定位数,正数空位补0,负数空位补1。例如:0b01001 >> 2 = 0b00010[01]0b10011 >> 2 = 0b11100[11](括号中的数是被裁掉的数)。

1
2
3
4
    0b01001         0b10011
>> 2 2
------------------------------
0b00010 0b11100

无符号右移(二元)[ >>> ]

  无符号右移在 C/C++ 中并不存在,在 C/C++ 中对无符号数进行右移便是无符号右移!

  无符号右移是将二进制数所有位向右移动指定位数,与右移不同,无符号右移空位永远补0。例如:0b10011 >>> 2 = 0b00100[11](括号中的数是被裁掉的数)。

1
2
3
4
    0b10011
>>> 2
--------------
0b00100