08.进制转换下篇:二、八、十六进制速转与补码表示
二进制、八进制和十六进制之间不必每次都经过十进制。因为 8 = 2^3、16 = 2^4,一个八进制位恰好对应 3 个二进制位,一个十六进制位恰好对应 4 个二进制位。本文系统讲解整数与小数的分组转换、八进制与十六进制互转,并补充位、字节、C 语言进制字面量、格式化输出、字符串解析、补码和位运算。
一、为什么可以直接分组转换
八进制和十六进制的基数都是 2 的整数次幂:
8 = 2^3
16 = 2^4
因此:
- 1 个八进制位可以完整表示 3 个二进制位;
- 1 个十六进制位可以完整表示 4 个二进制位;
- 二进制与八/十六进制之间的转换,本质上只是重新分组和替换符号,数值不会发生近似。
例如十进制 7 的二进制是 111,刚好是最大的三位二进制数,所以它也是最大的单个八进制数码。十进制 15 的二进制是 1111,对应最大的单个十六进制数码 F。
二、八进制与三位二进制对照表
| 0 | 000 | 0 |
| 1 | 001 | 1 |
| 2 | 010 | 2 |
| 3 | 011 | 3 |
| 4 | 100 | 4 |
| 5 | 101 | 5 |
| 6 | 110 | 6 |
| 7 | 111 | 7 |
这张表不必机械死记。三位二进制的位权是 4、2、1,例如:
101 = 1 x 4 + 0 x 2 + 1 x 1 = 5
110 = 1 x 4 + 1 x 2 + 0 x 1 = 6
三、十六进制与四位二进制对照表
| 0 | 0000 | 0 | 8 | 1000 | 8 |
| 1 | 0001 | 1 | 9 | 1001 | 9 |
| 2 | 0010 | 2 | A | 1010 | 10 |
| 3 | 0011 | 3 | B | 1011 | 11 |
| 4 | 0100 | 4 | C | 1100 | 12 |
| 5 | 0101 | 5 | D | 1101 | 13 |
| 6 | 0110 | 6 | E | 1110 | 14 |
| 7 | 0111 | 7 | F | 1111 | 15 |
四位二进制的位权是 8、4、2、1。例如:
1011 = 8 + 0 + 2 + 1 = 11 = B
1110 = 8 + 4 + 2 + 0 = 14 = E
四、二进制转八进制
转换规则:
1. 整数示例
把 (1010110)_2 转成八进制:
原数: 1010110
从右分组:1 | 010 | 110
左侧补零:001 | 010 | 110
对应八进制:1 | 2 | 6
因此:
(1010110)_2 = (126)_8
补在最左侧的 0 不改变数值,因为:
001010110 = 1010110
2. 带小数部分的示例
把 (110101.10111)_2 转成八进制:
整数部分:110 | 101 -> 6 | 5
小数部分:101 | 11 -> 101 | 110 -> 5 | 6
小数部分最后一组不足 3 位,要在右侧补 0:
(110101.10111)_2 = (65.56)_8
注意补零方向:
- 整数部分在最左侧补 0;
- 小数部分在最右侧补 0;
- 绝不能在靠近小数点的一侧随意补 0,否则会改变位权。
五、八进制转二进制
八进制转二进制更直接:把每个八进制数码替换成对应的 3 位二进制。
例如:
(572)_8
5 7 2
101 111 010
所以:
(572)_8 = (101111010)_2
带小数示例:
(17.34)_8
1 7 . 3 4
001 111 . 011 100
去掉整数部分无意义的前导 0:
(17.34)_8 = (1111.011100)_2
小数末尾的 0 可以省略而不改变数值:
(1111.011100)_2 = (1111.0111)_2
不过在表示固定宽度字段时,前导或尾随 0 可能具有格式意义,此时不应随意省略。
六、二进制转十六进制
规则与二进制转八进制相同,只是改为每 4 位一组。
1. 整数示例
把 (1010110)_2 转成十六进制:
原数: 1010110
从右分组:101 | 0110
左侧补零:0101 | 0110
对应十六进制: 5 | 6
所以:
(1010110)_2 = (56)_16
这与课堂笔记中十进制 86 的结果一致:
(86)_10 = (1010110)_2 = (126)_8 = (56)_16
2. 带小数部分的示例
仍使用 (110101.10111)_2:
整数部分:11 | 0101 -> 0011 | 0101 -> 3 | 5
小数部分:1011 | 1 -> 1011 | 1000 -> B | 8
因此:
(110101.10111)_2 = (35.B8)_16
和上一节结合可得:
(65.56)_8 = (110101.10111)_2 = (35.B8)_16
七、十六进制转二进制
把每个十六进制数码替换为固定 4 位二进制即可。
例如:
(A3F)_16
A 3 F
1010 0011 1111
所以:
(A3F)_16 = (101000111111)_2
带小数示例:
(2F.A)_16
2 F . A
0010 1111 . 1010
省略整体整数部分最前面的无意义 0 后:
(2F.A)_16 = (101111.1010)_2
十六进制的每一位始终展开为 4 位。即使 2 的普通二进制写法是 10,在分组中也要写成 0010,否则各组边界会错位。
八、八进制与十六进制怎样互转
八进制和十六进制之间没有整数个数码一一对应,因为 3 位与 4 位的分组边界不同。最稳妥的路线是:
八进制 -> 二进制 -> 十六进制
十六进制 -> 二进制 -> 八进制
1. 八进制转十六进制
把 (735)_8 转为十六进制。
先转成二进制:
7 3 5
111 011 101
再从右向左按 4 位分组:
111011101
= 0001 | 1101 | 1101
= 1 | D | D
所以:
(735)_8 = (1DD)_16
2. 十六进制转八进制
把 (2F.A)_16 转为八进制。
先转成二进制:
(2F.A)_16 = (00101111.1010)_2
再以小数点为界按 3 位分组:
整数:00 | 101 | 111 -> 0 | 5 | 7
小数:101 | 0 -> 5 | 0
去掉无意义的首尾 0:
(2F.A)_16 = (57.5)_8
全过程只补 0 和重新分组,没有使用十进制近似,所以对有限二进制小数是精确转换。
九、前导零、尾随零和补零位置
理解 0 的位置可以避免大量分组错误。
1. 可以不改变值的 0
00101 = 101
0.10100 = 0.101
整数部分左侧的前导零、小数部分右侧的尾随零不改变数值。
2. 会改变值的 0
101 != 1010
0.101 != 0.0101
整数部分右侧加 0 相当于乘以基数;小数点后靠左插入 0 会降低原有数码的位权。
3. 分组时的统一规则
可以记成一句话:
从小数点向两侧分组,不足位数时在最外侧补 0。
“最外侧”就是远离小数点的一侧。
十、位、字节与十六进制
1. 位和字节
bit 是二进制位,每位只能是 0 或 1。C 标准规定一个字节包含 CHAR_BIT 个比特,常见平台上 CHAR_BIT 为 8,可以通过 <limits.h> 查看。
#include <limits.h>
#include <stdio.h>
int main(void)
{
printf("CHAR_BIT = %d\\n", CHAR_BIT);
return 0;
}
不要把“1 字节必然等于 8 位”当成所有 C 实现的绝对规则,不过现代桌面、服务器和大多数嵌入式平台确实如此。
2. 为什么调试时常用十六进制
在 8 位字节的平台上:
1 个十六进制位 = 4 bit
2 个十六进制位 = 8 bit = 1 byte
一个字节的范围可以紧凑地写成:
二进制:00000000 ~ 11111111
十六进制:00 ~ FF
十进制:0 ~ 255
32 位值用二进制要写 32 个字符,用十六进制只需 8 个数码。例如:
11011110101011011011111011101111
= DEADBEEF
十六进制既紧凑,又能无损对应每 4 个比特,因此广泛用于地址、寄存器值、颜色、协议字段和机器码。
十一、C 语言中的进制整数常量
C 源代码通过前缀区分常见整数常量:
| 86 | 十进制 | 86 |
| 0126 | 八进制,前导 0 | 86 |
| 0x56 或 0X56 | 十六进制 | 86 |
| 0b1010110 | 二进制;C23 支持,较早 GCC 也提供过扩展 | 86 |
例如:
unsigned int decimal_value = 86U;
unsigned int octal_value = 0126U;
unsigned int hex_value = 0x56U;
它们的书写形式不同,但保存的数值相同。
二进制前缀需要格外注意:0b/0B 已进入 C23,而较旧 ISO C 标准没有这种语法;部分 GCC 版本和 GNU 模式早已把它作为扩展支持。追求旧标准可移植性时,可以继续使用十六进制,或用移位表达式表示位模式。
1. 八进制前导零陷阱
int a = 10; /* 十进制 10 */
int b = 010; /* 八进制 10,即十进制 8 */
因此,不要为了“对齐位数”随意给十进制整数常量补前导零。08 和 09 也不是合法的传统八进制整数常量,因为八进制没有数码 8 和 9。
2. 类型后缀
整数常量还可以带后缀:
U 或 u unsigned
L 或 l long
LL 或 ll long long
例如:
0xFFU
1000L
0xFFFFFFFFULL
进制前缀决定数值写法,类型后缀影响常量参与表达式时的候选类型,两者作用不同。
十二、使用 printf 输出不同进制
同一个整数可以按不同进制格式输出:
#include <stdio.h>
int main(void)
{
unsigned int value = 86U;
printf("decimal = %u\\n", value);
printf("octal = %o\\n", value);
printf("hex lower = %x\\n", value);
printf("hex upper = %X\\n", value);
printf("octal alt = %#o\\n", value);
printf("hex alt = %#X\\n", value);
return 0;
}
输出:
decimal = 86
octal = 126
hex lower = 56
hex upper = 56
octal alt = 0126
hex alt = 0X56
格式说明:
- %u:无符号十进制;
- %o:无符号八进制;
- %x:小写十六进制;
- %X:大写十六进制;
- #:使用“替代形式”,为非零十六进制结果添加 0x/0X,并保证八进制形式以 0 开头。
ISO C 的 printf 没有通用、可移植的 %b 二进制格式控制符。即使某些库提供扩展,也不应假定所有平台可用。
1. 自己输出二进制
#include <limits.h>
#include <stdio.h>
static void print_binary(unsigned int value)
{
int started = 0;
int width = (int)(sizeof(value) * CHAR_BIT);
for (int bit = width – 1; bit >= 0; —bit) {
unsigned int digit = (value >> bit) & 1U;
if (digit != 0U) {
started = 1;
}
if (started || bit == 0) {
putchar(digit != 0U ? '1' : '0');
}
}
}
int main(void)
{
unsigned int value = 86U;
print_binary(value);
putchar('\\n');
return 0;
}
输出:
1010110
如果需要观察固定宽度寄存器,应保留前导零,并明确使用的整数类型宽度,而不是通过 started 省略它们。
十三、把字符串按指定进制解析为整数
用户输入通常是字符串。标准库函数 strtol 可以按 2~36 的基数解析有符号长整数:
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
const char *text = "1010110";
char *end = NULL;
errno = 0;
long value = strtol(text, &end, 2);
if (end == text) {
fprintf(stderr, "没有可解析的数字\\n");
return 1;
}
if (*end != '\\0') {
fprintf(stderr, "存在非法字符:%s\\n", end);
return 1;
}
if (errno == ERANGE) {
fprintf(stderr, "数值超出 long 的范围\\n");
return 1;
}
printf("%ld\\n", value);
return 0;
}
这里第三个参数 2 明确指定二进制,因此输出十进制 86。
常见基数:
strtol("126", &end, 8); /* 八进制 */
strtol("86", &end, 10); /* 十进制 */
strtol("5A3B", &end, 16); /* 十六进制 */
基数传 0 时,函数会根据标准支持的前缀自动判断部分进制形式,但前导零可能让输入被解释为八进制。要求输入规则清晰时,显式传入基数通常更安全。
十四、固定宽度下的无符号整数
假设有 n 个二进制位,每一位都用于表示数值,则无符号范围是:
0 ~ 2^n – 1
例如 8 位无符号整数:
最小值:00000000 = 0
最大值:11111111 = 255
无符号整数运算按 2^n 取模。例如在恰好 8 位的无符号类型中:
255 + 1 -> 0
这不是数学整数无限范围下的结果,而是固定宽度机器整数的环绕规则。
C 的 <stdint.h> 在实现提供恰好对应宽度的类型时会定义:
uint8_t
uint16_t
uint32_t
uint64_t
嵌入式开发、二进制协议和硬件寄存器编程中,明确位宽通常比直接假设 int 宽度更可靠。
十五、有符号整数与补码
数学上可以写 -5,但内存中固定宽度整数需要用比特模式编码负数。现代计算机通常使用二进制补码。
1. 8 位补码表示 -5
先写出 +5:
00000101
逐位取反:
11111010
再加 1:
11111011
所以 8 位补码中的 -5 是:
11111011 = FB(十六进制)
同一组 8 位比特:
- 按无符号解释是 251;
- 按 8 位补码有符号整数解释是 -5。
比特没有自带类型,解释方式决定数值。
2. 为什么补码便于运算
在 8 位宽度中:
5 = 00000101
-5 = 11111011
相加 1 00000000
最高位之外的进位被丢弃,低 8 位为 0。加法电路可以统一处理正数和负数,这是补码的重要优势。
3. 补码必须明确位宽
-5 的 8 位和 16 位补码分别是:
8 位: 11111011 = FB
16 位: 1111111111111011 = FFFB
从较窄有符号数扩展到较宽类型时,负数通常进行符号扩展,即用最高符号位 1 填充新增高位。没有位宽,“某负数的补码是多少”就是不完整的问题。
4. 不要把最高位机械地称为负号
补码不是“最高位写 1,再在后面填绝对值”。例如 8 位 -5 是 11111011,而不是 10000101。最高位参与整体位权和补码运算,不能简单删除后把剩余位当绝对值。
十六、十六进制与位运算
十六进制常与掩码、寄存器和协议字段一起使用。
1. 按位与:提取某些位
unsigned int value = 0xABU;
unsigned int low_nibble = value & 0x0FU;
0x0F 的二进制是 00001111,按位与后保留低 4 位:
10101011 0xAB
& 00001111 0x0F
———–
00001011 0x0B
2. 按位或:设置某些位
flags |= 0x04U;
0x04 只有第 2 位为 1,因此该操作把对应位设置为 1,同时保留其他位。
3. 按位异或:翻转某些位
flags ^= 0x04U;
掩码为 1 的位被翻转,掩码为 0 的位保持不变。
4. 按位取反:翻转全部位
value = ~value;
结果取决于整数类型的实际宽度,并会受整数提升影响。操作窄类型时应特别注意转换后的类型和最终掩码。
5. 左移和右移
unsigned int mask = 1U << 5; /* 第 5 位设为 1 */
unsigned int part = value >> 4; /* 把高位字段移到低位 */
对可表示范围内的无符号值,左移 n 位与乘以 2^n 有紧密关系,右移 n 位相当于向下取整除以 2^n。移位数量不能为负,也必须小于提升后左操作数类型的位宽。
十七、进制、数据类型和字节序不是一回事
这三个概念经常混淆:
- 进制决定人如何书写数值;
- 数据类型决定比特宽度、范围和解释方式;
- 字节序决定多字节值在内存中的字节排列顺序。
例如数值 0x12345678 无论在大端还是小端机器上,打印为十六进制时仍是 12345678。差别只在内存地址从低到高排列为:
大端:12 34 56 78
小端:78 56 34 12
每个字节内部的位通常仍按正常数值理解。不能因为内存是小端,就把十六进制文本本身倒写成 87654321。
十八、在 Linux 终端快速验证
Bash 的 printf 可以辅助检查常见整数转换:
printf '%d\\n' 0x56
printf '%o\\n' 86
printf '%x\\n' 86
典型输出:
86
126
56
Python 也可以快速显示带前缀的结果:
python3 -c "n=86; print(bin(n), oct(n), hex(n))"
输出类似:
0b1010110 0o126 0x56
工具适合验证,但最好先理解分组与位权原理。否则遇到补码、固定位宽、小数精度或非法数码时,仍然难以判断工具结果的含义。
十九、常见错误与排查
1. 二进制分组方向错误
必须从小数点向外分组。没有小数点的整数从最右侧开始,不是从最左侧随意每 3 位或 4 位切开。
2. 补零补在靠近小数点的一侧
整数在左侧补,小数在右侧补。补错方向会改变位权和数值。
3. 八进制或十六进制展开时丢掉组内前导零
单个八进制位必须展开成 3 位,单个十六进制位必须展开成 4 位。中间组的前导零不能省略,否则后续重新分组会错位。
4. 把文本形式和内存形式混为一谈
字符串 "0x56" 在内存中是字符 '0'、'x'、'5'、'6' 的编码序列,不是一个字节数值 0x56。需要用 strtol 等函数解析后,才得到整数值。
5. 用 %d 输出无符号或不同宽度类型
printf 格式必须与实际参数类型匹配。普通 unsigned int 使用 %u、%o、%x;固定宽度类型可以使用 <inttypes.h> 提供的格式宏,避免跨平台宽度差异。
6. 忘记补码的位宽
负数的补码必须在固定宽度下讨论。FB 可以表示 8 位有符号的 -5,也可以作为更宽无符号数中的十进制 251;脱离类型和宽度无法唯一判断。
7. 把进制转换当成数值变化
(86)_10、(126)_8、(56)_16、(1010110)_2
表示的是同一个数学数值,只是书写方式不同。真正会改变结果的是溢出、截断、舍入或数据类型重新解释,而不是单纯换一种进制显示。
二十、总结
二进制与八进制、十六进制能够快速互转,是因为基数分别满足 8 = 2^3 和 16 = 2^4。从小数点向外按 3 位或 4 位分组,不足位数在最外侧补 0,就能完成整数和小数的精确转换。八进制与十六进制之间则以二进制作为桥梁重新分组。
进入 C 语言后,还要把“数值的进制写法”与“整数类型和内存编码”分开理解。前缀 0、0x、0b 控制源代码中的字面量写法,%o、%x 控制输出格式,strtol 负责把字符串解析为数值;补码、位宽和字节序则描述数值在机器中的编码与排列。理解这些边界,才能在嵌入式寄存器、网络协议、文件格式和位运算中正确使用二进制数据。






