第二 字符与编码
二进制能够表示数字,那能不能表示数字之外的各种文字,如英文、中文、俄语、法语等世界各个国家的文字?答案是肯定的,不仅可以表示各种文字,还能表示各种符号、图形(包括表情包)等。其中就使用到一种古老而又充满活力的技术——编码(coding)技术。
计算机中的编码技术,简单而言就是建立数字与字符之间一一对应的关系。比如用数字0到9分别表示其本身,10表示A,11表示B,12表示C,以此类推,就能解决所有字符的编码问题。很显然实际上存在无数种编码的方式。正所谓“无规矩不成方圆“,没有统一的编码规则是不行的。本章将系统介绍计算机中主流的字符编码方案,从经典的ASCII到现代的Unicode,以及Rust中的字符串处理。
2.1 ASCII编码
美国作为计算机的发明地,最先提出了一套编码方案——ASCII(American Standard Code for Information Interchange,美国信息交换标准代码),即ASCII码。
2.1.1 ASCII编码原理
ASCII码使用7位二进制数(共128个码位)来表示字符,其编码空间为:
$$ 0 \leq \text{ASCII码} \leq 127 \quad (0\text{x}00 \sim 0\text{x}7F) $$

ASCII码是计算机历史上最著名的编码方案之一,它最初是美国国家标准,供不同计算机在相互通信时用作共同遵守的西文字符编码标准,后来它被国际标准化组织(International Organization for Standardization, ISO)定为国际标准,称为ISO 646标准。
2.1.2 ASCII控制字符
ASCII码中,0~31和127为控制字符,用于控制设备的操作:
| 十进制 | 十六进制 | 缩写 | 含义 |
|---|---|---|---|
| 0 | 0x00 | NUL | 空字符 |
| 7 | 0x07 | BEL | 响铃 |
| 8 | 0x08 | BS | 退格 |
| 9 | 0x09 | HT | 水平制表符 |
| 10 | 0x0A | LF | 换行 |
| 13 | 0x0D | CR | 回车 |
| 27 | 0x1B | ESC | 转义 |
| 127 | 0x7F | DEL | 删除 |
2.1.3 ASCII可打印字符
32~126为可打印字符,包括数字、英文字母、标点符号等:
| 范围 | 内容 |
|---|---|
| 48~57 (0x30~0x39) | 数字 0~9 |
| 65~90 (0x41~0x5A) | 大写英文字母 A~Z |
| 97~122 (0x61~0x7A) | 小写英文字母 a~z |
| 32~47, 58~64, 91~96, 123~126 | 空格、标点及特殊符号 |
ASCII码的局限性也是显而易见的,它仅能表示128个字符,对于数以万计的中国汉字就显得十分无力了。
2.2 ANSI编码——各国方言
ANSI编码并非一个具体的编码标准,而是对不同国家和地区各自制定的多字节字符编码标准的统称。不同的国家和地区制定了不同的标准,由此产生了 GB2312、GBK、GB18030、Big5、Shift_JIS 等各自的编码标准。这些使用多个字节来代表一个字符的各种汉字延伸编码方式,统称为 ANSI 编码。
2.2.1 各国主要编码标准
| 编码标准 | 国家/地区 | 字节数 | 收录字符数 | 说明 |
|---|---|---|---|---|
| GB2312 | 中国大陆 | 1~2字节 | ~7,000 | 最早的中文编码标准 |
| GBK | 中国大陆 | 1~2字节 | ~21,000 | GB2312的扩展 |
| GB18030 | 中国大陆 | 1~4字节 | ~160万 | 国家标准,兼容Unicode |
| Big5 | 中国台湾 | 1~2字节 | ~13,000 | 繁体中文编码 |
| Shift_JIS | 日本 | 1~2字节 | ~12,000 | 日文编码 |
| EUC-KR | 韩国 | 1~2字节 | ~11,000 | 韩文编码 |
| ISO-8859-1 | 西欧 | 1字节 | 256 | Latin-1,西欧语言 |
2.2.2 ANSI编码的局限
ANSI编码的核心问题是:相同的字节序列在不同编码下可能表示不同的字符。例如,在简体中文Windows系统中,字节0xD6 0xD0表示“中“字(GB2312编码);而在日语系统中,相同的字节序列可能表示完全不同的字符。这种“方言“式的编码体系是产生乱码的根本原因。
2.3 Unicode编码——计算机世界中的“书同文“
秦始皇统一六国后,为了便于管理庞大的帝国,保证政令通畅,于是推行统一的文字和度量衡——“书同文,车同轨“不仅是秦始皇一项重要的历史贡献,也深刻影响和促成中国之后两千多年大一统的政治格局的形成。如今世界那么大,各国的文字、符号的数量是相当惊人的,并且随着社会的发展,新的字符不断涌现。字符的编码也需要与时俱进。Unicode(统一码,也叫万国码、单一码)应运而生,彻底解决全世界所有国家的文字、符号的编码问题。
2.3.1 Unicode编码空间
Unicode编码系统可分为编码方式和实现方式两个层次。Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。Unicode用数字0~0x10FFFF来映射这些字符,最多可以容纳1114112个字符,或者说有1114112个码位。码位(Code Point)就是可以分配给字符的数字,通常表示为 U+XXXX 的形式。
Unicode编码空间划分为若干平面(Plane):
| 平面 | 范围 | 名称 | 说明 |
|---|---|---|---|
| 第0平面 | U+0000 ~ U+FFFF | BMP(基本多文种平面) | 最常用的字符,包括基本拉丁字母、CJK统一表意文字等 |
| 第1平面 | U+10000 ~ U+1FFFF | SMP(多文种补充平面) | 古文字、音乐符号等 |
| 第2平面 | U+20000 ~ U+2FFFF | SIP(表意文字补充平面) | 罕用汉字 |
| 第3~13平面 | U+30000 ~ U+DFFFF | 保留 | 未分配 |
| 第14平面 | U+E0000 ~ U+EFFFF | SSP(特殊用途补充平面) | 格式控制字符 |
| 第15~16平面 | U+F0000 ~ U+10FFFF | PUA(私人使用区) | 用户自定义字符 |
2.3.2 UTF-8编码原理
UTF-8(Unicode Transformation Format - 8-bit)是一种变长编码方案,使用1~4个字节表示一个Unicode字符:
| Unicode码位范围 | UTF-8字节序列 | 字节数 |
|---|---|---|
| U+0000 ~ U+007F | 0xxxxxxx | 1 |
| U+0080 ~ U+07FF | 110xxxxx 10xxxxxx | 2 |
| U+0800 ~ U+FFFF | 1110xxxx 10xxxxxx 10xxxxxx | 3 |
| U+10000 ~ U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 4 |
UTF-8的编码规则可以用以下方式理解:
$$ \text{UTF-8字节数} = \begin{cases} 1 & \text{if } 0 \leq U \leq 0\text{x}7F \ 2 & \text{if } 0\text{x}80 \leq U \leq 0\text{x}7FF \ 3 & \text{if } 0\text{x}800 \leq U \leq 0\text{x}FFFF \ 4 & \text{if } 0\text{x}10000 \leq U \leq 0\text{x}10FFFF \end{cases} $$
编码示例:
| 字符 | Unicode码位 | UTF-8字节序列(十六进制) |
|---|---|---|
| ‘A’ | U+0041 | 41 |
| ‘中’ | U+4E2D | E4 B8 AD |
| ‘𠮷’(吉的异体字) | U+20BB7 | F0 A0 AE B7 |
| ‘💖’ | U+1F496 | F0 9F 92 96 |
UTF-8的优势在于:
- 向后兼容ASCII:纯ASCII文本在UTF-8下完全不变
- 无字节序问题:不需要BOM(虽然可以添加)
- 自同步:可以从任意字节位置开始解析
2.3.3 UTF-16编码原理
UTF-16使用2或4个字节表示一个Unicode字符:
- BMP内字符(U+0000 ~ U+FFFF):直接使用2字节表示
- 辅助平面字符(U+10000 ~ U+10FFFF):使用4字节(代理对)表示
代理对编码方式:
$$ \text{辅助平面字符编码} = \begin{cases} \text{高位代理} = \frac{U - 0\text{x}10000}{0\text{x}400} + 0\text{x}D800 \ \text{低位代理} = (U - 0\text{x}10000) \bmod 0\text{x}400 + 0\text{x}DC00 \end{cases} $$
其中,高位代理范围:0xD800~0xDBFF,低位代理范围:0xDC00~0xDFFF。
2.3.4 UTF-32编码原理
UTF-32使用固定4个字节表示所有Unicode字符,是最简单的编码方式:
$$ \text{UTF-32编码} = \text{Unicode码位值} $$
虽然UTF-32在字符定位上效率最高(O(1)随机访问),但由于每个字符固定占用4字节,对于以ASCII为主的文本,空间效率较低。
2.3.5 三种UTF编码对比
| 特性 | UTF-8 | UTF-16 | UTF-32 |
|---|---|---|---|
| 编码长度 | 变长(1~4字节) | 变长(2或4字节) | 定长(4字节) |
| 空间效率(ASCII文本) | 最优 | 较差(2倍) | 最差(4倍) |
| 空间效率(CJK文本) | 一般(3字节/字) | 较好(2字节/字) | 较差 |
| 随机访问 | O(n) | O(n) | O(1) |
| 字节序 | 无关 | 需要BOM | 需要BOM |
| 自同步 | 是 | 否 | 是 |
| 主要使用场景 | 网络传输、Linux/macOS | Windows、Java、JavaScript | 内部处理 |
2.3.6 BOM(字节顺序标记)
BOM(Byte Order Mark)是放在文本文件开头的特殊字符(U+FEFF),用于标识字节序:
| 编码 | BOM字节序列 | 说明 |
|---|---|---|
| UTF-8 | EF BB BF | 可选,通常不推荐 |
| UTF-16 BE | FE FF | 大端序 |
| UTF-16 LE | FF FE | 小端序(Windows默认) |
| UTF-32 BE | 00 00 FE FF | 大端序 |
| UTF-32 LE | FF FE 00 00 | 小端序 |
访问如下网站查看所有Unicode符号:
2.4 中文编码
中国程序员在职业生涯中,一定会遇到的一个问题就是中文乱码问题。操作系统、数据库、网络传输都有可能出现中文乱码问题,其本质就是使用了不恰当的编码对中文数据进行编码、解码。因此非常有必要了解各种场景下系统正在使用的编码方式。
2.4.1 中文编码标准演进
为了解决汉字的编码问题,中国制定了GB2312、GBK、GB18030等编码标准。
| 编码标准 | 发布时间 | 字节数 | 收录汉字数 | 特点 |
|---|---|---|---|---|
| GB2312 | 1980年 | 1~2字节 | 6,763 | 覆盖常用简体字,兼容ASCII |
| GBK | 1995年 | 1~2字节 | 21,003 | 扩展GB2312,含繁体字 |
| GB18030 | 2000年 | 1~4字节 | 160万+ | 国家标准,与Unicode完全映射 |
2.4.2 编码转换 iconv
生僻字 GB2312、GBK两种编码基本解决中文常用字符的编码,但中文中还有大量生僻字,需要GB18030才能编码。因此需要视具体情况采用恰当的编码。
# 查看文件编码
file -bi example.txt
# UTF-8转为GBK
iconv -f UTF-8 -t GBK 沁园春·雪.txt > snow.txt
# 忽略无法转换的字符
iconv -c UTF-8 -t GBK 沁园春·雪.txt > snow.txt
# GBK转为UTF-8
iconv -f GBK -t UTF-8 abc.txt > abc_1.txt
# UTF-8转为GB18030
iconv -f UTF-8 -t GB18030 生僻字.txt > gb18030.txt
2.4.3 操作系统的编码
Windows下中文常用支持中文编码有:简体中文(GB2312)、繁体中文(Big5)、UTF-8。
# Windows PowerShell
PS C:\Users\huang> chcp
活动代码页: 936
| 代码页 | 国家(地区)或语言 |
|---|---|
| 437 | 美国 |
| 932 | 日文(Shift-JIS) |
| 936 | 中国 - 简体中文(GB2312) |
| 949 | 韩文 |
| 950 | 繁体中文(Big5) |
| 1200 | Unicode |
| 1252 | 西欧(Windows) |
| 65001 | Unicode (UTF-8) |
ISO-8859-1(Latin-1) 是MySQL数据库默认编码,也是网络传输默认编码之一。
2.5 Rust中的字符串处理
Rust中默认使用的是UTF-8编码。这一设计决策与Rust的内存安全和零成本抽象理念高度一致。
2.5.1 Rust中的String、&str与Vec<u8>
Rust中的字符串类型设计体现了所有权和借用的核心概念:
| 类型 | 内存布局 | 所有权 | 说明 |
|---|---|---|---|
String | Vec<u8> 的包装 | 拥有 | 堆分配的UTF-8字符串,可增长 |
&str | 指向UTF-8字节序列的引用 | 借用 | 字符串切片,不可变引用 |
Vec<u8> | 字节数组 | 拥有 | 原始字节序列,不保证UTF-8合法性 |
char | 4字节(Unicode标量值) | 值类型 | 单个Unicode字符 |
fn main() {
// String:拥有所有权的UTF-8字符串
let mut s = String::from("hello");
s.push_str(" world");
println!("String: {}", s);
// &str:字符串切片(借用)
let slice: &str = &s;
println!("&str: {}", slice);
// String 与 &str 的关系
let owned: String = slice.to_string(); // &str -> String(拷贝)
let borrowed: &str = &owned; // String -> &str(借用)
// `Vec<u8>`:原始字节
let bytes: Vec<u8> = owned.into_bytes();
println!("`Vec<u8>`: {:?}", bytes);
// `Vec<u8>` -> String(需要验证UTF-8)
let s2 = String::from_utf8(bytes).unwrap();
println!("String from bytes: {}", s2);
}
2.5.2 Rust中的Unicode操作
fn main() {
// 从UTF-8字节构造字符串
let sparkle_heart_vec = vec![240, 159, 146, 150];
let sparkle_heart = String::from_utf8(sparkle_heart_vec).unwrap();
assert_eq!("💖", sparkle_heart);
let bytes = sparkle_heart.into_bytes();
assert_eq!(bytes, [240, 159, 146, 150]);
// Unicode转义序列
println!("\u{65b0}\u{534e}\u{793e}\u{5feb}\u{8baf}\u{ff1a}\u{0033}\u{6708}\u{0032}\u{0036}\u{65e5}\u{ff0c}\u{4e2d}\u{56fd}\u{548c}\u{6d2a}\u{90fd}\u{62c9}\u{65af}\u{5efa}\u{4ea4}\u{3002}");
println!("\u{5b78}\u{7fd2}\u{5f37}\u{570b} 伟大复兴");
// 字符转Unicode码位
println!("{:X} {:X}", '华' as u32, '夏' as u32);
// Emoji
println!("Rust常见emoji(表情符号):\u{1F1E8}\u{1F1F3}🦀💖🚀😂\u{1F980}\u{1F602}");
println!("常见emoji(国旗):\u{1F1E8}\u{1F1F3} 🇨🇳 🇺🇸 🇷🇺");
// ASCII转义
println!("ASCII常见字符:\u{0020}\u{0040}\u{0021}\u{0041}");
// 数学符号
println!("数学符号:∂∆∮\u{2211}\u{2200}\u{2208}\u{2209}\u{222b}");
}
2.5.3 String/&str/Vec<u8>转换关系(与第十九章呼应)
use std::str;
fn main() {
// &str -> String
let s: &str = "hello";
let owned: String = String::from(s);
let owned2: String = s.to_string();
assert_eq!(owned, owned2);
// &str -> &[u8]
let bytes: &[u8] = s.as_bytes();
println!("bytes: {:?}", bytes); // [104, 101, 108, 108, 111]
// String -> &str
let hello = String::from("hello");
let slice: &str = &hello;
let slice2: &str = hello.as_str();
assert_eq!(slice, slice2);
// String -> `Vec<u8>`
let hello = String::from("hello");
let bytes_vec: Vec<u8> = hello.into_bytes();
println!("into_bytes: {:?}", bytes_vec);
// &[u8] -> &str(可能失败)
let valid: &[u8] = b"hello";
let s: &str = str::from_utf8(valid).unwrap();
println!("from_utf8: {}", s);
let invalid: &[u8] = &[0xff, 0xfe]; // 非UTF-8
let result = str::from_utf8(invalid);
println!("invalid utf8: {:?}", result); // Err(Utf8Error)
// `Vec<u8>` -> String(可能失败)
let bytes = vec![104, 101, 108, 108, 111]; // "hello"的ASCII
let s = String::from_utf8(bytes).unwrap();
println!("from_utf8(Vec): {}", s);
}
2.6 数据库编码
数据库编码是中文乱码问题的另一个重灾区。MySQL数据库的历史遗留问题尤其值得关注。
2.6.1 MySQL字符集设置
-- 查看当前数据库字符集设置
show variables like 'character%';
2.6.2 utf8 vs utf8mb4
MySQL中的utf8实际上是utf8mb3的别名,只使用最多3个字节,无法存储4字节的Unicode字符(如Emoji、部分生僻字)。
-- utf8mb4:支持完整的Unicode,包括Emoji
UPDATE bakeries_db.user SET wx_nickname = 'abc🚀🎵✨' WHERE id = 5;
-- utf8mb3(旧版MySQL默认utf8)
-- Error Code: 1366. Incorrect string value: '\xF0\x9F\x9A\x80\xF0\x9F...' for column 'wx_nickname' at row 1
UPDATE uic.user SET wx_nickname = 'abc🚀🎵✨' WHERE id = 5;
| 字符集 | 最大字节数 | 支持范围 | 说明 |
|---|---|---|---|
| utf8mb3 | 3字节 | BMP平面 | MySQL旧版默认,不支持Emoji |
| utf8mb4 | 4字节 | 全部Unicode | 推荐使用,完整支持 |
建议:新项目务必使用
utf8mb4,并设置character_set_server=utf8mb4。
2.7 编码检测与转换
2.7.1 常见编码检测方法
// Rust中使用 encoding_rs 库进行编码检测和转换
use encoding_rs::{GBK, UTF_8};
fn main() {
// GBK编码的字节
let gbk_bytes: Vec<u8> = vec![0xD6, 0xD0, 0xCE, 0xC4]; // "中文"的GBK编码
// GBK -> UTF-8
let (cow, _, had_errors) = GBK.decode(&gbk_bytes);
if !had_errors {
let utf8_string = cow.into_owned();
println!("转换结果: {}", utf8_string); // 输出: 中文
}
// UTF-8 -> GBK
let utf8_str = "中文";
let (gbk_result, _, _) = UTF_8.encode(utf8_str);
println!("GBK字节: {:?}", gbk_result);
}
2.7.2 编码转换注意事项
- 有损转换:当目标编码无法表示源编码中的某些字符时,转换可能失败或产生替换字符
- BOM处理:UTF-8文件有时包含BOM,读取时需要注意跳过前3个字节
- 编码推断:没有绝对可靠的编码自动检测方法,最好明确指定编码
总结
| 编码方案 | 字节数 | 收录字符数 | 兼容性 | 主要使用场景 |
|---|---|---|---|---|
| ASCII | 1字节 | 128 | 所有编码兼容 | 英文文本、程序代码 |
| GB2312 | 1~2字节 | ~7,000 | 兼容ASCII | 早期中文系统 |
| GBK | 1~2字节 | ~21,000 | 兼容GB2312 | Windows中文系统 |
| GB18030 | 1~4字节 | ~160万 | 兼容GBK,映射Unicode | 中国政府标准 |
| UTF-8 | 1~4字节 | 111万+ | 兼容ASCII | Web、Linux、Rust |
| UTF-16 | 2或4字节 | 111万+ | 不兼容ASCII | Windows、Java |
| UTF-32 | 4字节 | 111万+ | 不兼容ASCII | 内部处理 |
练习题
-
ASCII编码范围:写出字符 ‘A’、‘a’、‘0’ 的ASCII码十进制值,并说明大小写字母ASCII码之间的关系。
-
UTF-8编码计算:手动计算汉字“中“(Unicode码位 U+4E2D)的UTF-8编码字节序列,并与
"中".as_bytes()的Rust输出进行对比。 -
Rust字符串操作:编写Rust代码,创建一个包含Emoji “🦀”(Unicode U+1F980)的
String,然后将其转换为Vec<u8>,打印每个字节的十六进制值,并验证其符合UTF-8编码规则。 -
编码转换陷阱:解释为什么以下Rust代码可能产生非预期结果,并给出修复方案:
#![allow(unused)] fn main() { let s = "中文"; let bytes: Vec<u8> = s.bytes().collect(); let recovered = String::from_utf8(bytes).unwrap(); println!("{}", recovered); } -
BOM处理:编写一个Rust函数
read_file_without_bom(path: &str) -> Result<String, std::io::Error>,读取UTF-8文本文件并自动跳过可能存在的BOM头。 -
编码检测:使用
encoding_rs库编写程序,读取一个未知编码的文本文件,尝试用GBK和UTF-8分别解码,输出两种解码结果供人工判断。 -
MySQL编码配置:写出创建数据库时指定
utf8mb4字符集的完整SQL语句,包括数据库、表和连接字符集的设置。 -
String/&str/
Vec<u8>关系图:画出Rust中String、&str、Vec<u8>、&[u8]之间的转换关系图,标注每个转换方法名称及其可能的失败情况。