跳到内容

第 3 卷 / 第 1 章 / 第 1 课

1.1 比特、字节与进制 ​

从上一卷的算法继续下探,数据最终会落到硬件能够表示和处理的比特模式。本节先建立机器表示模型,数值和文字都从这里出发。

第三卷从算法部分下到机器层。本篇先建立 bit、byte、对象表示和进位制;下一篇讨论Unicode 与 UTF,随后进入字节序与位掩码。

比特模式本身不等于“数字 42” ​

把一个字节写成 00101010,问它是什么。答案可以是无符号整数 42、ASCII 字符 *、颜色分量,或一条指令的一部分。位模式没有自带类型;存储格式和读取程序共同决定含义。

硬件用电压、电荷、磁化方向或其他物理状态承载信息,阈值取决于工艺与接口。把 1 固定说成 3.3V 或 5V 不适用于现代 CPU 内部。软件层只依赖抽象出的两种稳定状态。

一个 bit 有两种取值。n 个 bit 可区分 2^n 种模式。按无符号整数解释时,范围是 0 到 2^n-1。

本书默认 byte 指 8-bit octet,这是网络协议和现代通用平台的常见口径。ISO C 中,byte 是 char 的大小单位,位数由 CHAR_BIT 给出;标准保证至少 8,不强制所有实现恰好为 8。

位置决定一位的权重 ​

十进制 4205 等于 4*10^3 + 2*10^2 + 5。二、八、十六进制只是把底数换成 2、8、16。二进制 101010 等于 42。

十六进制一位恰好对应 4 bit,因此系统文档常用它压缩显示位模式:

text
0x2A = 0010 1010
0xFF = 1111 1111

八进制一位对应 3 bit。Unix 权限常写成 0755,因为每组三个权限位正好落在一个八进制数字中。

一份可编译的转换程序 ​

这段 C17 程序不假设 unsigned int 恰好 32 bit。把它保存为 encoding_numbers.c。缓冲区大小根据 sizeof 与 CHAR_BIT 计算,并报告当前实现一个 byte 有多少 bit。调用方提供至少 capacity 字节的可写缓冲区;空指针或容量不足返回失败,且不改写输出。

c
#include <limits.h>
#include <stdio.h>

static int to_binary(unsigned int value, char *output, size_t capacity) {
    char reversed[sizeof(unsigned int) * CHAR_BIT];
    size_t length = 0;

    if (output == NULL || capacity < 2) {
        return 0;
    }
    if (value == 0) {
        output[0] = '0';
        output[1] = '\0';
        return 1;
    }

    while (value != 0) {
        reversed[length++] = (char)('0' + value % 2U);
        value /= 2U;
    }
    if (length + 1 > capacity) {
        return 0;
    }
    for (size_t index = 0; index < length; index++) {
        output[index] = reversed[length - index - 1];
    }
    output[length] = '\0';
    return 1;
}

int main(void) {
    char binary[sizeof(unsigned int) * CHAR_BIT + 1];
    if (!to_binary(42U, binary, sizeof binary)) {
        return 1;
    }

    printf("CHAR_BIT=%d\n", CHAR_BIT);
    printf("42 = %s (base 2) = %#o (base 8) = %#x (base 16)\n",
           binary, 42U, 42U);
    return 0;
}

编译并运行:

bash
cc -std=c17 -Wall -Wextra -Werror encoding_numbers.c -o encoding_numbers
./encoding_numbers

在 8-bit byte 的常见平台上,输出是:

text
CHAR_BIT=8
42 = 101010 (base 2) = 052 (base 8) = 0x2a (base 16)

C17 没有标准二进制整数字面量。0b101010 在一些编译器中是扩展;C23 才把 0b / 0B 纳入标准。以 0 开头的整数字面量按八进制解释,010 是十进制 8,这个历史语法很容易制造配置错误。

从十进制手算到任意进制 ​

把非负整数转换到基数 b,可反复除以 b,记录余数,再逆序读取。以 42 转二进制为例:

除法商余数
42 / 2210
21 / 2101
10 / 250
5 / 221
2 / 210
1 / 201

从下向上得到 101010。反向转换可从左到右重复 result = result * base + digit。

手算时先估计范围。8 个 bit 装不下 256;两个十六进制位最多表示 0xFF = 255。范围判断能在正式计算前发现错位。

内存里保存的是对象表示 ​

对有效且可读取的对象,C 允许通过 unsigned char * 查看其 byte 表示。下面是另一个独立程序,保存为 object_bytes.c,不要把两个 main 放进同一文件。对象检查器没有给数据换类型,只是逐 byte 读取表示:

c
#include <stdint.h>
#include <stdio.h>

static void print_bytes(const void *object, size_t size) {
    const unsigned char *bytes = object;
    for (size_t index = 0; index < size; index++) {
        printf("%02X%s", (unsigned int)bytes[index], index + 1 == size ? "\n" : " ");
    }
}

int main(void) {
    uint32_t value = UINT32_C(0x12345678);
    print_bytes(&value, sizeof value);
    return 0;
}

输出按低地址到高地址排列,具体顺序留到字节序章节解释。不要把某台机器打印出的顺序写进可移植文件格式;跨平台数据应显式规定字段宽度和 byte 顺序。

对象表示可能包含 padding bit 或 padding byte,不能依靠 padding 的值比较两个对象的业务内容。uint32_t 则是可选的精确宽度无符号类型:实现若提供它,就恰好有 32 个值位且没有 padding bit。

bit、byte 与容量单位 ​

名称含义
bit(b)一个二值位置
byte(B)本书指 8 bit;ISO C 中是可寻址的字符存储单位
octet明确的 8 bit 单位
KiB1024 byte
MiB1024 KiB
kBSI 口径下 1000 byte

网络带宽常用 bit/s,文件容量常用 byte。100 Mb/s 的理想链路对应 12.5 MB/s 的原始传输率;应用有效载荷还要扣除协议开销。单位大小写不是排版细节。

常见边界 ​

  • 数值写法不是存储格式。42、052 与 0x2A 在 C 源码中表示同一个值。
  • printf 格式符必须与实参类型匹配,否则会产生未定义行为。
  • 负整数不能套用本页的无符号除基转换。下一章会从补码与有符号范围开始。
  • 8 bit 有 256 种模式,最大无符号值是 255。
  • 解析二进制协议时不要把缓冲区直接强制转换成结构体指针;对齐、padding、字节序和别名规则都会参与结果。

动手检查表示 ​

  1. 把 to_binary 扩展到 2–16 任意基数,并拒绝非法基数。
  2. 手算 0x7F、0x80、0755 的十进制值。
  3. 查询编译器的 CHAR_BIT、sizeof(int) 与 UINT_MAX。
  4. 解释为什么 RGB 颜色 #FF8040 适合用十六进制展示。
  5. 区分一个字符、一个 Unicode 码点和 UTF-8 中的一个 byte。

字节流开始出现文字 ​

到这里,我们只给位模式赋予了数值含义。下一篇进入字符集、Unicode 与 UTF 编码:同一个“字符”可能涉及码点、编码单元、byte 和用户看到的字形,这几层必须拆开。

Built with VitePress | Software Systems Atlas