进制与进制转换

本篇是总篇 00-信息的表示与处理 2.2 节的展开:什么是进制、生活里到处都是进制、计算机里的 0b/0/0x,以及全套转换方法(简单拆分法 + 反复乘除法)。

一、何为进制

进制也就是进位计数制的简称,是人为定义的带进位的计数方法。

对于任何一种进制——X 进制,表示每一个数位上的数,运算时都是逢 X 进一位。

例如:十进制是逢十进一,十六进制是逢十六进一,二进制就是逢二进一,八进制是逢八进一,以此类推,X 进制就是逢 X 进一。

十进制进位-5ce3009a

二、生活中的进制

进制根本不是计算机的发明,它早就长在生活里了。从定义出发逐个对号入座:

进制 逢几进一 生活例子
二进制 逢二进一 两只鞋子 = 1 双鞋;两个抓手 = 1 双手
三进制 逢三进一 3 个月 = 1 个季度
四进制 逢四进一 4 个季度 = 1 年
七进制 逢七进一 7 天 = 1 周
十进制 逢十进一 0、1、2、…、9、10、…(最常用)
十二进制 逢十二进一 12 瓶啤酒 = 1 打
二十四进制 逢二十四进一 24 小时 = 1 天
六十进制 逢六十进一 60 秒 = 1 分钟;60 分钟 = 1 小时

所以"进制"这个概念一点不玄——只是换一套"凑满就进位"的规则而已。

三、计算机中的进制

计算机里常用的进制有三种,C 语言里各有各的前缀:

1、二进制:前缀 0b

以 0b 作为前缀,跟上 0 和 1 组成的数字:

#include <stdio.h>
int main() {
    int a = 0b101;
    printf("%d\n", a);
    return 0;
}

输出:

5

因为 %d 代表输出的数是十进制,所以需要将二进制转换成十进制以后输出,0b101 在十进制下的值为 5。数字比较小时可以直接数出来:二进制下 101 对应十进制下的 5。

2、八进制:前缀 0

讲八进制之前,先看一段代码:

#include <stdio.h>
int main() {
    int a = 0123;
    printf("%d\n", a);
    return 0;
}

那么,这段代码的输出值是多少呢?

83

嗯,如果没学过 C 语言,肯定认为这是 123,实际上它的答案是 83。

为什么呢?参考二进制的表示法,八进制的表示法是前缀 1 个 0,然后跟上 0-7 的数字。换言之,我们需要把 123 这个八进制数转换成十进制后再输出,转换结果就是 83。这里数字较大,已经没法一个一个数出来了,所以需要进行进制转换——方法马上讲。

3、十六进制:前缀 0x

以 0x 或者 0X 作为前缀,跟上 0-9、a-f、A-F 的数字,大小写字母含义相同,分别代表 10 到 15:

#include <stdio.h>
int main() {
    int a = 0X123;
    printf("%d\n", a);
    return 0;
}

输出的是 291。

所以这里就涉及到了十六进制转换成十进制,接下来看进制转换。

四、进制转换

先解决 2、8、16 之间的互转(也就是开头引入的问题——把一连串 01 表示成较简短的串),再解决它们与十进制的互转。

4.1 简单拆分法(2、8、16 互转)

能"拆"的前提是 2、8、16 存在特殊关系:

  • 2³ = 8 → 3 位二进制 = 1 位八进制
  • 2⁴ = 16 → 4 位二进制 = 1 位十六进制

所以这类转换可以直接靠记忆和推算。

二进制与十六进制

把二进制数 4 位 4 位地拆分,用数字 0~9 以及字符 A~F 表示 16 个可能的值:

image-ae62d2ad

记住A 是 1010、C 是 1100、F 是 1111、4 是 0100、8 是 1000,其他的就能很容易推出来了——记住 A C F 对应的十进制值,剩下的 B D E 通过相对关系算出来就行。

练一下就会了:

16→2

给出 0x173A4C:

    1       7       3      A        4      C
 0001    0111    0011   1010    0100   1100

2→16

反过来,给出 11 1100 1010 1101 1011 0011,从右到左划分、4 个 4 个一组,剩余的自动用 0 补齐:

0011 1100 1010 1101 1011 0011
  3      C      A      D      B      3

再练俩:

0x39A7F8  0011 1001 1010 1111 1000
(对照 A=1010、F=1111、8=1000 逐位展开)

10 0110 1110 0111 1011 0101
 0010 0110 1110 0111 1011 0101
   2      6      E      7      B      5

如果带小数

(注意:因为精度问题,结果不会是准确的,很少问这类问题;硬是碰到,可以用这种方式解决)

以小数点为中心,往左往右 4 个 4 个划分:整数部分最左边不满 4 位就补 0,小数部分最右边没满 4 位不用管。

比如 101.01011:

划分成:0101 . 0101 1
就是:  5 . 5 1

16→2 反过来:小数点右边的最后一个数得补前导 0:

    5   .   5       1
 0101   0101    0001
= 0101.0101 1

二进制与八进制

把二进制数 3 位 3 位地拆分,用数字 0~7 表示 8 个可能的值,要记的东西也一样:

0(000) 1(001) 2(010) 3(011)
4(100) 5(101) 6(110) 7(111)

2→8

对于二进制 1111000010,三个化作一组、左边补零:

001 111 000 010
  1   7   0   2

8→2

对于八进制 423:

  4    2    3
100  010  011

若存在小数

整数部分和小数部分分开:整数部分 3 位一组从右往左划分,不足 3 位左侧补 0;小数部分 3 位一组从左往右划分,不足 3 位右侧补 0。

(还是那句话,这玩意不准确,可以别管)

八进制与十六进制

八进制和十六进制之间没有直接通道,以二进制为中介

16→8

F2C4B 先换成 2 进制再换成 8 进制:

1111 0010 1100 0100 1011        ← 每位十六进制展开成 4 位二进制
011 110 010 110 001 001 011     ← 从右边开始三个化作一组,左边补零
  3   6   2   6   1   1   3

8→16 反过来:先把八进制换成对应的 2 进制,再以 4 位为一个单元做划分。

4.2 反复乘除法(与十进制互转)

2、8、16 的相互转换解决了,接下来研究它们与十进制的转换。

先说结论:X 进制转十进制、十进制转 X 进制,有着一套模板套路。如果看不懂公式,后面有各个进制的实例。

套路

1、X 进制 → 十进制

对 X 进制的数定义三个概念:

  • 【概念1】数字部分从右往左编号为 0 到 n,第 i 个数字位记作 di,这个数字就是 dn…d1d0
  • 【概念2】每个数字位有一个权值
  • 【概念3】第 i 个数字位的权值是 X^i

基于以上概念,X 进制转十进制 = 每一位数字 × 它的权值,再累加

image-93f3bea6

举个例子,八进制数 0123 转十进制,套公式:

image-da6a25be
  • 1 权值最高,8 的 2 次
  • 2 权值次之,8 的 1 次
  • 3 权值最低,8 的 0 次

这样就把一个八进制的数转换成了十进制。为了加深印象,前文提到的十六进制数 0x123,套用同样的公式:

image-5560a5ee

2、十进制 → X 进制

从 X 进制转十进制的原理可知,任何一个十进制数字都是由 X 进制的幂的倍数累加而成。所以,一个数一定有 X⁰ 这部分,而这部分可以通过原数除以 X 的余数得到。然后把原数除以 X 后得到的商,肯定又有 X⁰ 的部分,就这样重复试除,直到商为 0 时结束——过程中的余数逆序一下,就是对应进制的数了。

对于整数部分采用除 x 取余法,对于小数部分采用乘 x 取整法

image-18730e2f

如果看了这里还是不太理解,接下来把 2、8、16 分别与 10 进制的转换各过一遍。

十六进制与十进制

十六进制 → 十进制

用相应的十六的幂乘以每个十六进制数字:

7AF(H) = 1967(D)

从 F 开始右往左数:F 为第 0 位,A 为第 1 位,7 为第 2 位

7×16² + 10×16¹ + 15×16⁰
= 1792 + 160 + 15 = 1967

(计算量很大,所以一般也看不见这种题,都是十进制换其他)

十进制 → 十六进制

将十进制数字 x 转化为十六进制,可以反复用 x 除以 16,得到一个商 q 和一个余数 r:

  • 把余数 r 作为最低位数字,把商 q 继续参与循环运算
  • x = q×16 + r
  • 反复对 q 执行该操作直到商为 0,其余数组合起来的数(逆序)就是转换结果

可以叫做除 16 取余法

314156(D) = 4CB2C(H)

314156 / 16 = 19634 …… 12    12 就是 C → C
19634  / 16 = 1227  …… 2     2 还是 22C
1227   / 16 = 76    …… 11    11 是 B → B2C
76     / 16 = 4     …… 12    12 是 C → CB2C
4      / 16 = 0     …… 4     4 还是 44CB2C

(这其实就是用一个栈去接收每轮余数,然后出栈即逆序输出——大一的时候也用程序实现过这一过程)

如果加上小数部分

道理一样,把小数分成整数部分和小数部分。比如 58.75 转成十六进制:

整数部分的 58,还是除 16 取余法

58 / 16 = 3 …… 10    → A
 3 / 16 = 0 …… 33A

小数部分 0.75,用的是乘 16 取整法

0.75 × 16 = 12.003A.C
0.00 × 16 =  0.003A.C0
……

能观察到两个不同点:

  1. 这回取下来的是整数部分,留下小数部分进入下一轮乘法运算
  2. 这回取出的整数不是逆序,而是顺序地放在小数点后面

(这个例子不是很好看得出这一点,后面 10 换 2 能更直观看到)

十进制与二进制

二进制 → 十进制

1111 1111

1×2⁷ + 1×2⁶ + 1×2⁵ + 1×2⁴ + 1×2³ + 1×2² + 1×2¹ + 1×2⁰
= 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1
= 255
image-07420aec

十进制 → 二进制

直接用带小数的例子:123.45,分为 123 和 0.45 两部分。

整数部分的 123,除 2 取余法

123 / 2 = 61 …… 1     1
 61 / 2 = 30 …… 1     11
 30 / 2 = 15 …… 0     011
 15 / 2 = 7  …… 1     1011
  7 / 2 = 3  …… 1     11011
  3 / 2 = 1  …… 1     111011
  1 / 2 = 0  …… 1     1111011

小数部分 0.45,乘 2 取整法

0.45 × 2 = 0.9    1111011.0
0.9  × 2 = 1.8    1111011.01
0.8  × 2 = 1.6    1111011.011
0.6  × 2 = 1.2    1111011.0111
0.2  × 2 = 0.4    1111011.01110
0.4  × 2 = 0.8    1111011.011100

还可以继续往下,反正就是取整数部分往后加、取小数部分进入下一轮乘法运算

这里有个应试小技巧:题目里一般算完整数部分就能排除两个答案,再算小数,算到 4、5 位结果肯定就出来了。

image-18730e2f

比如这道题:以下二进制的值与十进制 23.456 最接近的是?

10111.0101 / 11011.1111 / 11011.0111 / 10111.0111

23.456 可拆分为整数部分 = 23、小数部分 = 0.456。

整数部分用除 2 取余法

23 ÷ 2 = 111    1
11 ÷ 2 = 51    11
 5 ÷ 2 = 21    111
 2 ÷ 2 = 10    0111
 1 ÷ 2 = 01    10111

逆序排列余数,整数部分的二进制为 10111——排除两个选项了。

小数部分用乘 2 取整法

0.456 × 2 = 0.912,取整数部分为 0    10111.0
0.912 × 2 = 1.824,取整数部分为 1    10111.01
0.824 × 2 = 1.648,取整数部分为 1    10111.011
0.648 × 2 = 1.296,取整数部分为 1    10111.0111

已经得到答案:11011.0111

八进制与十进制

八进制 → 十进制

3725(O) = 3×8³ + 7×8² + 2×8¹ + 5×8⁰
        = 1536 + 448 + 16 + 5
        = 2005(D)

十进制 → 八进制

1770.625 换八进制:

整数部分,除 8 取余法

1770 ÷ 8 = 2212    2
 221 ÷ 8 = 275    52
  27 ÷ 8 = 33    352
   3 ÷ 8 = 03    3352

小数部分,乘 8 取整法

0.625 × 8 = 5.0    3352.5
0.0   × 8 = 0.0    3352.50

五、小结

整套东西收敛下来其实就四句话:

方向 方法 关键点
十进制 → 其他(整数部分) 循环除以目标进制 余数逆序存放
十进制 → 其他(小数部分) 循环乘目标进制取整 整数部分顺序接在小数点后
其他 → 十进制 按权展开再累加 个位权重为 0,往左递增、往右递减
2、8、16 互转 以二进制为中介拆分 3 个 3 个划是八进制,4 个 4 个划是十六进制

这些过程都可以用程序去实现(循环加栈),现在就懒得做这意义不大的事情了——大一上学期写过,代码就不贴出来了,又臭又长(毕竟当时啥也不会)。

六、盲点自测

  1. int a = 0123; 输出为什么不是 123? ——前缀 0 是八进制标志,123 要按八进制转十进制输出,得 83。
  2. 十六进制的 A~F 大小写有区别吗? ——没有,0x2A 和 0x2a 相同。
  3. 为什么 2、8、16 之间能直接"拆位"? ——因为 8=2³、16=2⁴,3 位/4 位二进制恰好对应 1 位八/十六进制。
  4. 十进制转 X 进制,为什么余数要逆序? ——第一轮余数是最低位(X⁰ 那部分),越往后权重越高,所以最后算出的反而是最高位。
  5. 小数部分为什么用乘法而不是除法? ——小数是"负幂"部分,乘 X 把 X⁻¹ 顶成个位,顶出来的整数就是当前小数位的值;除法只会越除越小。
  6. 带小数的进制转换结果为什么常常不精确? ——乘法取整可能无限循环(比如 0.45 那串),二进制只能近似表示,这和总篇里"连续与离散"一节是同一件事。

进制这一关过了,数字在计算机里的"写法"就没问题了。但正数好办,负数怎么表示才是重头戏 → 02-原码反码补码

⬅️ 总篇 🏠 00-信息的表示与处理 ➡️ 02-原码反码补码