摘要
一个 std::string 保存的是字节而不是字母,而在今天的文本中,一个字母可能占一到四个字节。本课区分日常说法混为一谈的三样东西:抽象字符,Unicode 用一个称为码位的数来标识它;UTF-8 用来编码它的字节;以及屏幕画出的字形。本课说明 UTF-8 如何把一个码位的各位分配到它的各个字节中,为什么“新年”占六个字节,以及如何不解码就数出一个字符串的字符。随后展示哪些操作会破坏文本(逐字节反转、逐字节转大写、在一个字符中间截断),哪些操作无须任何小心就能正常工作(保存、复制、拼接、查找、输出),以及 C++23 标准库不做什么:ASCII 以外的大小写转换、按语言的字母顺序、规范化。
学习目标
- 说明 UTF-8 如何把一个码位编码成一到四个字节,并手工解码“ñ”的字节。
- 预测含有非 ASCII 字母的字符串以字节计的大小,例如
"新年"(6)或"程序设计"(12)。 - 通过数出不是后续字节的字节来数出 UTF-8 字符串的字符,所用条件为
(c & 0xC0) != 0x80。 - 识别破坏 UTF-8 文本的操作(逐字节反转、按字节调用
std::toupper、在一个字符中间截断),并决定每种情况下的做法。
字符、码位与字节
三个层次
字符、布尔值、类型转换与常量一课把 char 介绍为一个按 ASCII 表写成字母的单字节整数,并提醒过 '山' 放不进它;编译、运行与配置一课说明了 "新年" 占六个字节,并设置好控制台来输出它。本课解释其中的原因。ASCII 为 128 个字符分配了编号,从 0 到 127,用七位就能容纳:英文字母、数字、标点符号和一些控制码。然而,真实的文本含有汉字、带重音符号和分音符的字母、希腊字母、数学符号和表情符号,总数达数十万。由此可见,首先需要一张大得多的表,其次需要一种把表中编号写成字节的方法。
应当区分三个层次。第一个是抽象字符:字母“小写 eñe”,与它如何保存或如何画出无关。Unicode 是为每个抽象字符分配一个数的标准,这个数就是它的码位,写作 U+ 后跟至少四位十六进制数字:“ñ”是 U+00F1(241),“A”是 U+0041(65,与 ASCII 中的编号相同),“€”是 U+20AC(8364)。码位从 0 到 U+10FFFF,略多于一百万个。第二个层次是编码:把每个码位变成一串字节的规则。UTF-8 是网络、现今的系统和本课程所用的编码。第三个层次是呈现:字体在屏幕上画出的字形,它可以随字体而变,而字符不变。
如果是这样,那么在知道其编码之前,一份文本数据还不是文本:字节 c3 b1 在 UTF-8 中是“ñ”,但用旧式控制台的码表来读,就是两个与它毫无关系的符号,这正是1.4一课教过如何诊断的故障。同一个问题从文档排版的角度来看,出现在 LaTeX 学术写作课程的“语言与字体”一课中。
查看字节
要查看一个字符串的字节,遍历它就够了:std::string 和 std::string_view 都是 char 的序列,基于范围的 for 循环逐个交出它们的字节。要以十六进制写出这些字节,可用格式说明 {:02x}(两位十六进制数字,左侧补零)。与2.4一课中的 {:d} 一样,由于 2023 年通过、GCC 15 已实施的同一项标准修正,{:02x} 把 char 当作无符号字节写出。这条规则很重要,因为在 x86-64 上的 GCC 中 char 有符号:字节 c3 转换为 int 后是数 ,{:02x} 会把它写成 -3d。
#include <print>#include <string_view>// 一段文本的字节。// 前置条件:无。// 后置条件:输出一行,含 text、它以字节计的大小和每个字节的十六进制值。void printBytes(std::string_view text){ std::print("{:<4}{}:", text, text.size()); for (const char c : text) { std::print(" {:02x}", c); } std::println("");}int main(){ printBytes("A"); printBytes("ñ"); printBytes("€"); printBytes("😀"); printBytes("新年");}A 1: 41ñ 2: c3 b1€ 3: e2 82 ac😀 4: f0 9f 98 80新年6: e6 96 b0 e5 b9 b4每一行显示文本、它的大小和它的字节。每种长度各一个字符:“A”占一个字节,“ñ”占两个,“€”占三个,表情符号占四个。“新年”这个词占六个字节,e6 96 b0 和 e5 b9 b4,它的 size() 是 6,不是 2。请注意,“A”是字节 41,与 ASCII 中相同:UTF-8 写 ASCII 的 128 个字符时与 ASCII 完全一样。
图示自上而下跟踪“ñ”的编码:码位、它的各位、这些位的分配,以及得到的两个字节,其中前缀位被突出显示。
证明
“ñ”是码位
U+00F1,即 。用二进制, 写作
11110001。它大于 127 且小于 2048,所以 UTF-8 用两个字节来写它,这两个字节运载十一位:在左侧补零直到十一位,得到00011110001。这十一位分成两组:前五位
00011和后六位110001。第一个字节带前缀
110,它宣告一个两字节的字符,后接第一组的五位:11000011。第二个字节带前缀10,它标志一个后续字节,后接其余六位:10110001。用十六进制,
1100 0011是c3,1011 0001是b1:正是前面的程序为“ñ”写出的两个字节。二进制与十六进制之间的换算,每四位一组,就是代数与几何 I 课程中“进位记数制”一课的换算。
正在加载内容…
已完成
登录账户以保存您的进度