欢迎光临
我们一直在努力

08.进制转换下篇:二、八、十六进制速转与补码表示

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

四、二进制转八进制

转换规则:

  • 以小数点为界分别处理整数和小数部分;
  • 整数部分从右向左每 3 位一组;
  • 小数部分从左向右每 3 位一组;
  • 不足 3 位时,在远离小数点的一侧补 0;
  • 每组按对照表换成一个八进制数码。
  • 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 负责把字符串解析为数值;补码、位宽和字节序则描述数值在机器中的编码与排列。理解这些边界,才能在嵌入式寄存器、网络协议、文件格式和位运算中正确使用二进制数据。

    赞(0)
    未经允许不得转载:171主机测评 » 08.进制转换下篇:二、八、十六进制速转与补码表示
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址