进制与进制转换
本篇是总篇 00-信息的表示与处理 2.2 节的展开:什么是进制、生活里到处都是进制、计算机里的 0b/0/0x,以及全套转换方法(简单拆分法 + 反复乘除法)。
一、何为进制
进制也就是进位计数制的简称,是人为定义的带进位的计数方法。
对于任何一种进制——X 进制,表示每一个数位上的数,运算时都是逢 X 进一位。
例如:十进制是逢十进一,十六进制是逢十六进一,二进制就是逢二进一,八进制是逢八进一,以此类推,X 进制就是逢 X 进一。
二、生活中的进制
进制根本不是计算机的发明,它早就长在生活里了。从定义出发逐个对号入座:
| 进制 | 逢几进一 | 生活例子 |
|---|---|---|
| 二进制 | 逢二进一 | 两只鞋子 = 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 个可能的值:
记住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 进制转十进制 = 每一位数字 × 它的权值,再累加:
举个例子,八进制数 0123 转十进制,套公式:
- 1 权值最高,8 的 2 次
- 2 权值次之,8 的 1 次
- 3 权值最低,8 的 0 次
这样就把一个八进制的数转换成了十进制。为了加深印象,前文提到的十六进制数 0x123,套用同样的公式:
2、十进制 → X 进制
从 X 进制转十进制的原理可知,任何一个十进制数字都是由 X 进制的幂的倍数累加而成。所以,一个数一定有 X⁰ 这部分,而这部分可以通过原数除以 X 的余数得到。然后把原数除以 X 后得到的商,肯定又有 X⁰ 的部分,就这样重复试除,直到商为 0 时结束——过程中的余数逆序一下,就是对应进制的数了。
对于整数部分采用除 x 取余法,对于小数部分采用乘 x 取整法:
如果看了这里还是不太理解,接下来把 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 还是 2 → 2C
1227 / 16 = 76 …… 11 11 是 B → B2C
76 / 16 = 4 …… 12 12 是 C → CB2C
4 / 16 = 0 …… 4 4 还是 4 → 4CB2C
(这其实就是用一个栈去接收每轮余数,然后出栈即逆序输出——大一的时候也用程序实现过这一过程)
如果加上小数部分
道理一样,把小数分成整数部分和小数部分。比如 58.75 转成十六进制:
整数部分的 58,还是除 16 取余法:
58 / 16 = 3 …… 10 → A
3 / 16 = 0 …… 3 → 3A
小数部分 0.75,用的是乘 16 取整法:
0.75 × 16 = 12.00 → 3A.C
0.00 × 16 = 0.00 → 3A.C0
……
能观察到两个不同点:
- 这回取下来的是整数部分,留下小数部分进入下一轮乘法运算
- 这回取出的整数不是逆序,而是顺序地放在小数点后面
(这个例子不是很好看得出这一点,后面 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
十进制 → 二进制
直接用带小数的例子: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 位结果肯定就出来了。
比如这道题:以下二进制的值与十进制 23.456 最接近的是?
10111.0101 / 11011.1111 / 11011.0111 / 10111.0111
23.456 可拆分为整数部分 = 23、小数部分 = 0.456。
整数部分用除 2 取余法:
23 ÷ 2 = 11 余 1 1
11 ÷ 2 = 5 余 1 11
5 ÷ 2 = 2 余 1 111
2 ÷ 2 = 1 余 0 0111
1 ÷ 2 = 0 余 1 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 = 221 余 2 2
221 ÷ 8 = 27 余 5 52
27 ÷ 8 = 3 余 3 352
3 ÷ 8 = 0 余 3 3352
小数部分,乘 8 取整法:
0.625 × 8 = 5.0 3352.5
0.0 × 8 = 0.0 3352.50
五、小结
整套东西收敛下来其实就四句话:
| 方向 | 方法 | 关键点 |
|---|---|---|
| 十进制 → 其他(整数部分) | 循环除以目标进制 | 余数逆序存放 |
| 十进制 → 其他(小数部分) | 循环乘目标进制取整 | 整数部分顺序接在小数点后 |
| 其他 → 十进制 | 按权展开再累加 | 个位权重为 0,往左递增、往右递减 |
| 2、8、16 互转 | 以二进制为中介拆分 | 3 个 3 个划是八进制,4 个 4 个划是十六进制 |
这些过程都可以用程序去实现(循环加栈),现在就懒得做这意义不大的事情了——大一上学期写过,代码就不贴出来了,又臭又长(毕竟当时啥也不会)。
六、盲点自测
int a = 0123;输出为什么不是 123? ——前缀 0 是八进制标志,123 要按八进制转十进制输出,得 83。- 十六进制的 A~F 大小写有区别吗? ——没有,0x2A 和 0x2a 相同。
- 为什么 2、8、16 之间能直接"拆位"? ——因为 8=2³、16=2⁴,3 位/4 位二进制恰好对应 1 位八/十六进制。
- 十进制转 X 进制,为什么余数要逆序? ——第一轮余数是最低位(X⁰ 那部分),越往后权重越高,所以最后算出的反而是最高位。
- 小数部分为什么用乘法而不是除法? ——小数是"负幂"部分,乘 X 把 X⁻¹ 顶成个位,顶出来的整数就是当前小数位的值;除法只会越除越小。
- 带小数的进制转换结果为什么常常不精确? ——乘法取整可能无限循环(比如 0.45 那串),二进制只能近似表示,这和总篇里"连续与离散"一节是同一件事。
进制这一关过了,数字在计算机里的"写法"就没问题了。但正数好办,负数怎么表示才是重头戏 → 02-原码反码补码
⬅️ 总篇 🏠 00-信息的表示与处理 ➡️ 02-原码反码补码
💬 评论