数的表示与编码
进制转换、原码反码补码、IEEE 754浮点数、ASCII与Unicode编码。
1. 进制与转换
计算机内部使用二进制,但人类习惯十进制。不同进制是同一数值的不同表示方式。
1.1 常用进制
| 进制 | 基数 | 数字符号 | 前缀 | 示例 |
|---|---|---|---|---|
| 二进制 | 2 | 0, 1 | 0b | 0b1010 |
| 八进制 | 8 | 0~7 | 0 | 012 |
| 十进制 | 10 | 0~9 | 无 | 10 |
| 十六进制 | 16 | 0 | 0x | 0xA |
同一个数在不同进制下的表示:
0b1010=012=10=0xA
1.2 任意进制 → 十进制
方法:按权展开求和。每一位的权值 = 数字 × 基数^位序号(从右往左,从0开始)。
二进制 1011 → 十进制:
1×2³ + 0×2² + 1×2¹ + 1×2⁰
= 8 + 0 + 2 + 1
= 11
八进制 17 → 十进制:
1×8¹ + 7×8⁰
= 8 + 7
= 15
十六进制 0xFF → 十进制:
15×16¹ + 15×16⁰
= 240 + 15
= 255
1.3 十进制 → 任意进制
方法:除基取余,逆序排列。
十进制 25 → 二进制:
25 ÷ 2 = 12 ... 余1 ← 最低位
12 ÷ 2 = 6 ... 余0
6 ÷ 2 = 3 ... 余0
3 ÷ 2 = 1 ... 余1
1 ÷ 2 = 0 ... 余1 ← 最高位
结果: 11001
十进制 255 → 十六进制:
255 ÷ 16 = 15 ... 余15 (F) ← 最低位
15 ÷ 16 = 0 ... 余15 (F) ← 最高位
结果: FF
1.4 二进制 ↔ 八进制
方法:每3位二进制对应1位八进制(因为 2³ = 8)。
二进制 011 010 111 → 八进制
011 = 3
010 = 2
111 = 7
结果: 327
八进制 527 → 二进制
5 = 101
2 = 010
7 = 111
结果: 101 010 111
1.5 二进制 ↔ 十六进制
方法:每4位二进制对应1位十六进制(因为 2⁴ = 16)。
二进制 1010 1111 → 十六进制
1010 = A
1111 = F
结果: AF
十六进制 0xC3 → 二进制
C = 1100
3 = 0011
结果: 1100 0011
1.6 代码实现进制转换
#include <stdio.h>
#include <string.h>
// 十进制整数转二进制字符串
void dec_to_bin(int n, char* buf, int size) {
if (n == 0) {
strcpy(buf, "0");
return;
}
int i = 0;
int is_negative = 0;
if (n < 0) { is_negative = 1; n = -n; }
while (n > 0 && i < size - 1) {
buf[i++] = (n % 2) + '0';
n /= 2;
}
// 反转
for (int j = 0; j < i / 2; j++) {
char tmp = buf[j];
buf[j] = buf[i - 1 - j];
buf[i - 1 - j] = tmp;
}
buf[i] = '\0';
}
int main() {
char buf[33];
dec_to_bin(25, buf, sizeof(buf));
printf("25 的二进制: %s\n", buf); // 11001
return 0;
}
2. 原码、反码与补码
计算机需要表示正数和负数。8位二进制中,最高位作为符号位:0表示正数,1表示负数。
2.1 原码
原码是最直观的表示法:最高位为符号位,其余位为数值的绝对值。
+5 的原码: 0 0000101
-5 的原码: 1 0000101
+0 的原码: 0 0000000
-0 的原码: 1 0000000 ← 存在+0和-0两种表示
8位原码范围:-127 ~ +127(共255个数,因为0有两个表示)
2.2 反码
- 正数的反码与原码相同
- 负数的反码:符号位不变,其余位按位取反
+5 的反码: 0 0000101 (与原码相同)
-5 的反码: 1 1111010 (原码数值位取反)
+0 的反码: 0 0000000
-0 的反码: 1 1111111 ← 仍然有两个0
8位反码范围:-127 ~ +127
2.3 补码
- 正数的补码与原码相同
- 负数的补码 = 反码 + 1
+5 的补码: 0 0000101 (与原码相同)
-5 的补码: 1 1111011 (反码 11111010 + 1)
+0 的补码: 0 0000000
-0 的补码: 1 00000000 → 溢出后为 00000000 ← 只有一个0!
8位补码范围:-128 ~ +127(共256个数)
2.4 为什么用补码?
补码解决了三个关键问题:
① 统一了0的表示
原码: +0 = 00000000, -0 = 10000000 → 两个0
补码: +0 = 00000000, -0 = 00000000 → 只有一个0
② 加减法统一
补码让减法可以转化为加法运算,CPU不需要单独的减法器:
5 - 3 = 5 + (-3)
00000101 (+5的补码)
+ 11111101 (-3的补码)
-----------
00000010 (+2的补码) ← 结果正确!
(最高位进位自然丢弃)
③ 扩大了表示范围
8位补码可以表示 -128,这是原码和反码做不到的:
-128 的补码: 10000000
这个编码在原码和反码中没有对应值
在补码中直接定义为 -128
2.5 补码的快速求法
负数补码的两种求法:
方法1: 原码 → 反码 → 反码+1 → 补码
-6 原码: 10000110
-6 反码: 11111001
-6 补码: 11111010
方法2: 从右往左找到第一个1,该1及其右边的0保持不变,左边各位取反
-6 原码: 1 0000110
↑ 第一个1
保持: 10
取反: 1 1111010
结果: 11111010 ← 与方法1一致
2.6 补码运算与溢出
#include <stdio.h>
#include <limits.h>
int main() {
// 补码溢出示例
signed char a = 127; // 最大正值
signed char b = 1;
signed char c = a + b; // 溢出!
printf("127 + 1 = %d\n", c); // 输出: -128(发生了溢出)
// 判断溢出:两个正数相加得到负数,或两个负数相加得到正数
signed char x = -128;
signed char y = -1;
signed char z = x + y; // 溢出!
printf("-128 + (-1) = %d\n", z); // 输出: 127(发生了溢出)
return 0;
}
2.7 8位各表示法范围汇总
| 表示法 | 范围 | 0的个数 | 特点 |
|---|---|---|---|
| 原码 | -127 ~ +127 | 2个 | 直观,但运算复杂 |
| 反码 | -127 ~ +127 | 2个 | 原码到补码的过渡 |
| 补码 | -128 ~ +127 | 1个 | 现代计算机标准表示 |
3. 浮点数表示(IEEE 754)
计算机用浮点数表示小数,遵循 IEEE 754 标准。
3.1 科学记数法回顾
十进制: 123.456 = 1.23456 × 10²
二进制: 101.101 = 1.01101 × 2²
3.2 IEEE 754 单精度(32位)
| 1位符号 | 8位指数 | 23位尾数 |
| S | E | M |
值 = (-1)^S × 1.M × 2^(E-127)
| 部分 | 位数 | 说明 |
|---|---|---|
| 符号位 S | 1 | 0=正数,1=负数 |
| 指数 E | 8 | 偏移量127,实际指数 = E - 127 |
| 尾数 M | 23 | 隐含前导1,实际尾数 = 1.M |
3.3 IEEE 754 双精度(64位)
| 1位符号 | 11位指数 | 52位尾数 |
| S | E | M |
值 = (-1)^S × 1.M × 2^(E-1023)
3.4 浮点数转换示例
将 -6.5 转换为 IEEE 754 单精度浮点数:
第1步: 确定符号位
-6.5 为负数 → S = 1
第2步: 转换为二进制
6 = 110
0.5 = 0.1
6.5 = 110.1
第3步: 规格化
110.1 = 1.101 × 2²
第4步: 提取各字段
S = 1
M = 101 (后面补0至23位: 10100000000000000000000)
E = 2 + 127 = 129 = 10000001
第5步: 组合
1 10000001 10100000000000000000000
= 0xC0D00000
3.5 浮点数的特殊值
| 指数 E | 尾数 M | 含义 | 说明 |
|---|---|---|---|
| 全0 | 全0 | ±0 | 正零或负零 |
| 全0 | 非全0 | 非正规数 | 非常接近0的数 |
| 全1 | 全0 | ±∞ | 正无穷或负无穷 |
| 全1 | 非全0 | NaN | 不是一个数(0/0等) |
3.6 浮点数精度问题
#include <stdio.h>
int main() {
float a = 0.1f;
float b = 0.2f;
float c = a + b;
// 0.1 + 0.2 不等于 0.3!
printf("0.1 + 0.2 = %.20f\n", c); // 0.30000001192092895508
printf("0.3 = %.20f\n", 0.3f); // 0.30000001192092895508
// 正确的比较方式:使用误差范围
float epsilon = 1e-6f;
if (c - 0.3f < epsilon && 0.3f - c < epsilon) {
printf("近似相等\n");
}
// 绝对不要用 == 比较浮点数
if (c == 0.3f) {
printf("相等\n"); // 可能不会执行
}
return 0;
}
3.7 精度与范围对比
| 类型 | 位数 | 有效数字 | 范围 |
|---|---|---|---|
| float | 32 | ~7位 | ±1.18×10⁻³⁸ ~ ±3.4×10³⁸ |
| double | 64 | ~15位 | ±2.23×10⁻³⁰⁸ ~ ±1.80×10³⁰⁸ |
实际开发中,优先使用
double以获得更高精度。仅在内存受限(如嵌入式、GPU计算)时使用float。
4. ASCII 码
ASCII(American Standard Code for Information Interchange)是最基础的字符编码标准,用7位二进制表示128个字符。
4.1 ASCII 码表(0~127)
控制字符(0~31):
| 范围 | 含义 | 常见字符 |
|---|---|---|
| 0 | NUL(空字符) | 字符串结束标志 ‘\0’ |
| 7 | BEL(响铃) | |
| 8 | BS(退格) | ‘\b’ |
| 9 | HT(水平制表) | ‘\t’ |
| 10 | LF(换行) | ‘\n’ |
| 13 | CR(回车) | ‘\r’ |
| 27 | ESC(转义) | |
| 32 | SP(空格) | ’ ’ |
可打印字符(32~127):
| 字符类别 | 范围 | 关键记忆点 |
|---|---|---|
| 空格 | 32 | 第一个可打印字符 |
| 数字 0~9 | 48~57 | ’0’ = 48 |
| 大写 A~Z | 65~90 | ’A’ = 65 |
| 小写 a~z | 97~122 | ’a’ = 97 |
| DEL | 127 | 最后一个ASCII字符 |
4.2 必须记住的 ASCII 值
'0' = 48 'A' = 65 'a' = 97
'1' = 49 'B' = 66 'b' = 98
... ... ...
'9' = 57 'Z' = 90 'z' = 122
关键规律:
- 数字、大写字母、小写字母的编码各自连续
- 大写字母 + 32 = 对应小写字母(
'A' + 32 = 'a') - 数字字符 - ‘0’ = 数值(
'7' - '0' = 7)
#include <stdio.h>
int main() {
// 大小写转换
char upper = 'G';
char lower = upper + 32; // 或 upper + ('a' - 'A')
printf("%c -> %c\n", upper, lower); // G -> g
// 字符转数字
char digit = '7';
int value = digit - '0';
printf("'%c' = %d\n", digit, value); // '7' = 7
// 判断字符类型
char ch = '5';
if (ch >= '0' && ch <= '9')
printf("数字\n");
else if (ch >= 'A' && ch <= 'Z')
printf("大写字母\n");
else if (ch >= 'a' && ch <= 'z')
printf("小写字母\n");
return 0;
}
4.3 扩展 ASCII(128~255)
标准ASCII只有128个字符,无法表示中文等非英文字符。扩展ASCII(128~255)在不同编码页中有不同含义,这导致了乱码问题,也是Unicode诞生的背景。
5. Unicode 与 UTF-8
5.1 Unicode 概述
Unicode 为世界上几乎所有文字系统中的每个字符分配了唯一的码点(Code Point)。
码点范围: U+0000 ~ U+10FFFF(共1,114,112个码点)
表示方式: U+ 后跟4~6位十六进制数
示例:
'A' → U+0041
'中' → U+4E2D
'' → U+1F600
5.2 Unicode 平面
Unicode 码点空间被划分为17个平面,每个平面包含 65,536 个码点:
| 平面 | 范围 | 内容 |
|---|---|---|
| 第0平面(BMP) | U+0000 ~ U+FFFF | 基本多文种平面,最常用 |
| 第1平面 | U+10000 ~ U+1FFFF | 补充多文种平面 |
| 第2平面 | U+20000 ~ U+2FFFF | CJK统一表意文字扩展 |
| 第3~13平面 | … | 保留或分配 |
| 第14平面 | U+E0000 ~ U+EFFFF | 补充特殊用途平面 |
| 第15~16平面 | U+F0000 ~ U+10FFFF | 私用区 |
日常使用的汉字大多在 BMP(U+4E00~U+9FFF)和第2平面中。
5.3 UTF-8 编码规则
UTF-8 是 Unicode 的一种变长编码实现,用1~4个字节表示一个码点:
| 码点范围 | 字节数 | 编码格式 |
|---|---|---|
| U+0000 ~ U+007F | 1 | 0xxxxxxx |
| U+0080 ~ U+07FF | 2 | 110xxxxx 10xxxxxx |
| U+0800 ~ U+FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 ~ U+10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
5.4 码点 → UTF-8 转换示例
例1:‘A’ = U+0041 → 1字节
U+0041 = 1000001
范围 U+0000~U+007F → 1字节格式: 0xxxxxxx
填入: 01000001
结果: 0x41(与ASCII完全兼容!)
例2:‘中’ = U+4E2D → 3字节
U+4E2D = 0100 1110 0010 1101
范围 U+0800~U+FFFF → 3字节格式: 1110xxxx 10xxxxxx 10xxxxxx
将 0100111000101101 填入 x 的位置:
1110 0100 10 111000 10 101101
= E4 = B8 = AD
结果: E4 B8 AD(3个字节)
例3:” = U+1F600 → 4字节
U+1F600 = 0001 1111 0110 0000 0000
范围 U+10000~U+10FFFF → 4字节格式: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
将 000011111011000000000 填入 x 的位置(共21位):
11110 000 10 011111 10 011000 10 000000
= F0 = 9F = 98 = 80
结果: F0 9F 98 80(4个字节)
5.5 UTF-8 的设计优势
| 优势 | 说明 |
|---|---|
| ASCII 兼容 | U+0000~U+007F 的 UTF-8 编码与 ASCII 完全相同 |
| 自同步 | 从任意字节可以判断是首字节还是后续字节 |
| 无字节序问题 | 不需要 BOM(字节序标记) |
| 容错性好 | 损坏一个字节只影响一个字符,不影响后续 |
| 空间效率 | 英文1字节,中文3字节,比UTF-32节省空间 |
5.6 BOM(字节序标记)
UTF-16 和 UTF-32 存在字节序问题,用 BOM 标识:
FE FF → 大端序(Big Endian,高位在前)
FF FE → 小端序(Little Endian,低位在前)
UTF-8 的 BOM: EF BB BF(可选,不推荐添加)
#include <stdio.h>
// 检测文件BOM
void check_bom(const unsigned char* data, int len) {
if (len >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF)
printf("UTF-8 BOM\n");
else if (len >= 2 && data[0] == 0xFE && data[1] == 0xFF)
printf("UTF-16 Big Endian BOM\n");
else if (len >= 2 && data[0] == 0xFF && data[1] == 0xFE)
printf("UTF-16 Little Endian BOM\n");
else
printf("无BOM\n");
}
5.7 编码方案对比
| 编码 | 字节数 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| UTF-8 | 1~4 | 兼容ASCII,无字节序 | 中文占3字节 | Web、文件存储 |
| UTF-16 | 2或4 | 中文2字节 | 有字节序,英文2字节 | Windows内部、Java |
| UTF-32 | 固定4 | 定长,随机访问快 | 空间浪费大 | 内存中处理 |
6. 小结
| 主题 | 核心要点 |
|---|---|
| 进制转换 | 按权展开求和(→十进制),除基取余(十进制→) |
| 原码反码补码 | 补码统一0的表示、统一加减法、扩大表示范围 |
| IEEE 754 | 符号位+指数+尾数,注意精度问题和特殊值 |
| ASCII | ’0’=48, ‘A’=65, ‘a’=97,大小写差32 |
| Unicode/UTF-8 | 码点唯一标识字符,UTF-8变长编码兼容ASCII |
理解数的表示与编码是理解程序行为的基础——为什么 0.1 + 0.2 != 0.3、为什么中文占3个字节、为什么整数溢出会变成负数,这些问题的答案都在本章中。