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,因此系统文档常用它压缩显示位模式:
0x2A = 0010 1010
0xFF = 1111 1111八进制一位对应 3 bit。Unix 权限常写成 0755,因为每组三个权限位正好落在一个八进制数字中。
一份可编译的转换程序
这段 C17 程序不假设 unsigned int 恰好 32 bit。把它保存为 encoding_numbers.c。缓冲区大小根据 sizeof 与 CHAR_BIT 计算,并报告当前实现一个 byte 有多少 bit。调用方提供至少 capacity 字节的可写缓冲区;空指针或容量不足返回失败,且不改写输出。
#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;
}编译并运行:
cc -std=c17 -Wall -Wextra -Werror encoding_numbers.c -o encoding_numbers
./encoding_numbers在 8-bit byte 的常见平台上,输出是:
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 / 2 | 21 | 0 |
| 21 / 2 | 10 | 1 |
| 10 / 2 | 5 | 0 |
| 5 / 2 | 2 | 1 |
| 2 / 2 | 1 | 0 |
| 1 / 2 | 0 | 1 |
从下向上得到 101010。反向转换可从左到右重复 result = result * base + digit。
手算时先估计范围。8 个 bit 装不下 256;两个十六进制位最多表示 0xFF = 255。范围判断能在正式计算前发现错位。
内存里保存的是对象表示
对有效且可读取的对象,C 允许通过 unsigned char * 查看其 byte 表示。下面是另一个独立程序,保存为 object_bytes.c,不要把两个 main 放进同一文件。对象检查器没有给数据换类型,只是逐 byte 读取表示:
#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 单位 |
| KiB | 1024 byte |
| MiB | 1024 KiB |
| kB | SI 口径下 1000 byte |
网络带宽常用 bit/s,文件容量常用 byte。100 Mb/s 的理想链路对应 12.5 MB/s 的原始传输率;应用有效载荷还要扣除协议开销。单位大小写不是排版细节。
常见边界
- 数值写法不是存储格式。
42、052与0x2A在 C 源码中表示同一个值。 printf格式符必须与实参类型匹配,否则会产生未定义行为。- 负整数不能套用本页的无符号除基转换。下一章会从补码与有符号范围开始。
- 8 bit 有 256 种模式,最大无符号值是 255。
- 解析二进制协议时不要把缓冲区直接强制转换成结构体指针;对齐、padding、字节序和别名规则都会参与结果。
动手检查表示
- 把
to_binary扩展到 2–16 任意基数,并拒绝非法基数。 - 手算
0x7F、0x80、0755的十进制值。 - 查询编译器的
CHAR_BIT、sizeof(int)与UINT_MAX。 - 解释为什么 RGB 颜色
#FF8040适合用十六进制展示。 - 区分一个字符、一个 Unicode 码点和 UTF-8 中的一个 byte。
字节流开始出现文字
到这里,我们只给位模式赋予了数值含义。下一篇进入字符集、Unicode 与 UTF 编码:同一个“字符”可能涉及码点、编码单元、byte 和用户看到的字形,这几层必须拆开。