详解Unicode与UTF-8、UTF-16、UTF-32.

article/2025/11/11 11:09:12

计算机起源于美国,上个世纪,他们对英语字符与二进制位之间的关系做了统一规定,并制定了一套字符编码规则,这套编码规则被称为ASCII编码

ASCII 编码一共定义了128个字符的编码规则,用七位二进制表示 ( 0x00 - 0x7F ), 这些字符组成的集合就叫做 ASCII 字符集

随着计算机的普及,在不同的地区和国家又出现了很多字符编码,比如: 大陆的 GB2312、港台的 BIG5, 日本的 Shift JIS等等

由于字符编码不同,计算机在不同国家之间的交流变得很困难,经常会出现乱码的问题,比如:对于同一个二进制数据,不同的编码会解析出不同的字符

当互联网迅猛发展,地域限制打破之后,人们迫切的希望有一种统一的规则, 对所有国家和地区的字符进行编码,于是 Unicode 就出现了

Unicode 简介

Unicode 是国际标准字符集,它将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换

Unicode 字符集的编码范围是 0x0000 - 0x10FFFF , 可以容纳一百多万个字符, 每个字符都有一个独一无二的编码,也即每个字符都有一个二进制数值和它对应,这里的二进制数值也叫 码点 , 比如:汉字 "中" 的 码点是 0x4E2D, 大写字母 A 的码点是 0x41, 具体字符对应的 Unicode 编码可以查询 Unicode字符编码表

字符集和字符编码

字符集是很多个字符的集合,例如 GB2312 是简体中文的字符集,它收录了六千多个常用的简体汉字及一些符号,数字,拼音等字符

字符编码是 字符集的一种实现方式,把字符集中的字符映射为特定的字节或字节序列,它是一种规则

比如:Unicode 只是字符集,UTF-8、UTF-16、UTF-32 才是真正的字符编码规则

Unicode 字符存储

Unicode 是一个符号集, 它只规定了每个符号的二进制值,但是符号具体如何存储它并没有规定

前面提到, Unicode 字符集的编码范围是 0x0000 - 0x10FFFF,因此需要 1 到 3 个字节来表示

那么,对于三个字节的 Unicode字符,计算机怎么知道它表示的是一个字符而不是三个字符呢 ?

如果所有字符都用三个字节表示,那么对于那些一个字节就能表示的字符来说,有两个字节是无意义的,对于存储来说,这是极大的浪费,假如 , 一个普通的文本, 大部分字符都只需一个字节就能表示,现在如果需要三个字节才能表示,文本的大小会大出三倍左右

因此,Unicode 出现了多种存储方式,常见的有 UTF-8、UTF-16、UTF-32,它们分别用不同的二进制格式来表示 Unicode 字符

UTF-8、UTF-16、UTF-32 中的 "UTF" 是 "Unicode Transformation Format" 的缩写,意思是"Unicode 转换格式",后面的数 字表明至少使用多少个比特位来存储字符, 比如:UTF-8 最少需要8个比特位也就是一个字节来存储,对应的, UTF-16 和 UTF-32 分别需要最少 2 个字节 和 4 个字节来存储

UTF-8 编码

UTF-8: 是一种变长字符编码,被定义为将码点编码为 1 至 4 个字节,具体取决于码点数值中有效二进制位的数量

UTF-8 的编码规则:

  1. 对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的, 所以 UTF-8 能兼容 ASCII 编码,这也是互联网普遍采用 UTF-8 的原因之一

  1. 对于 n 字节的符号( n > 1),第一个字节的前 n 位都设为 1,第 n + 1 位设为 0,后面字节的前两位一律设为 10 。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码

下表是Unicode编码对应UTF-8需要的字节数量以及编码格式

Unicode编码范围(16进制)

UTF-8编码方式(二进制)

000000 - 00007F

0xxxxxxx ASCII码

000080 - 0007FF

110xxxxx 10xxxxxx

000800 - 00FFFF

1110xxxx 10xxxxxx 10xxxxxx

01 0000 - 10 FFFF

11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

表格中第一列是Unicode编码的范围,第二列是对应UTF-8编码方式,其中红色的二进制 "1" 和 "0" 是固定的前缀, 字母 x 表示可用编码的二进制位

根据上面表格,要解析 UTF-8 编码就很简单了,如果一个字节第一位是 0 ,则这个字节就是一个单独的字符,如果第一位是 1 ,则连续有多少个 1 ,就表示当前字符占用多少个字节

下面以 "中" 字 为例来说明 UTF-8 的编码,具体的步骤如下图, 为了便于说明,图中左边加了 1,2,3,4 的步骤编号

首先查询 "中" 字的 Unicode 码 0x4E2D, 转成二进制, 总共有 16 个二进制位, 具体如上图 步骤1 所示

通过前面的 Unicode 编码和 UTF-8 编码的表格知道,Unicode 码 0x4E2D 对应 000800 - 00FFFF 的范围,所以, "中" 字的 UTF-8 编码 需要 3 个字节,即格式是 1110xxxx 10xxxxxx 10xxxxxx

然后从 "中" 字的最后一个二进制位开始,按照从后向前的顺序依次填入格式中的 x 字符,多出的二进制补为 0, 具体如上图 步骤2、步骤3 所示

于是,就得到了 "中" 的 UTF-8 编码是 11100100 10111000 10101101, 转换成十六进制就是 0xE4B8AD, 具体如上图 步骤4 所示

UTF-16 编码

UTF-16 也是一种变长字符编码, 这种编码方式比较特殊, 它将字符编码成 2 字节 或者 4 字节

具体的编码规则如下:

  1. 对于 Unicode 码小于 0x10000 的字符, 使用 2 个字节存储,并且是直接存储 Unicode 码,不用进行编码转换

  1. 对于 Unicode 码在 0x10000 和 0x10FFFF 之间的字符,使用 4 个字节存储,这 4 个字节分成前后两部分,每个部分各两个字节,其中,前面两个字节的前 6 位二进制固定为 110110,后面两个字节的前 6 位二进制固定为 110111, 前后部分各剩余 10 位二进制表示符号的 Unicode 码 减去 0x10000 的结果

  1. 大于 0x10FFFF 的 Unicode 码无法用 UTF-16 编码

下表是Unicode编码对应UTF-16编码格式

Unicode编码范围(16进制)

具体Unicode码(二进制)

UTF-16编码方式(二进制)

字节

0000 0000 - 0000 FFFF

xxxxxxxx xxxxxxxx

xxxxxxxx xxxxxxxx

2

0001 0000 - 0010 FFFF

yy yyyyyyyy xx xxxxxxxx

110110yy yyyyyyyy 110111xx xxxxxxxx

4

表格中第一列是Unicode编码的范围,第二列是 具体Unicode码的二进制 ( 第二行的第二列表示的是 Unicode 码 减去 0x10000 后的二进制 ) , 第三列是对应UTF-16编码方式,其中红色的二进制 "1" 和 "0" 是固定的前缀, 字母 x 和 y 表示可用编码的二进制位, 第四列表示 编码占用的字节数

前面提到过,"中" 字的 Unicode 码是 4E2D, 它小于 0x10000,根据表格可知,它的 UTF-16 编码占两个字节,并且和 Unicode 码相同,所以 "中" 字的 UTF-16 编码为 4E2D

我从 Unicode字符表网站 找了一个老的南阿拉伯字母, 它的 Unicode 码是: 0x10A6F , 可以访问 https://unicode-table.com/cn/10A6F/ 查看字符的说明, Unicode 码对应的字符如下图所示

下面以这个 老的南阿拉伯字母的 Unicode 码 0x10A6F 为例来说明 UTF-16 4 字节的编码,具体步骤如下,为了便于说明,图中左边加了 1,2,3,4 、5的步骤编号

首先把 Unicode 码 0x10A6F 转成二进制, 对应上图的 步骤 1

然后把 Unicode 码 0x10A6F 减去 0x10000, 结果为 0xA6F 并把这个值转成二进制 00 00000010 10 01101111,对应上图的 步骤 2

然后 从二进制 00 00000010 10 01101111 的最后一个二进制为开始,按照从后向前的顺序依次填入格式中的 x 和 y 字符,多出的二进制补为 0, 对应上图的 步骤 3、 步骤 4

于是,就计算出了 Unicode 码 0x10A6F 的 UTF-16 编码是 11011000 00000010 11011110 01101111 , 转换成十六进制就是 0xD802DE6F, 对应上图的 步骤 5

UTF-32 编码

UTF-32 是固定长度的编码,始终占用 4 个字节,足以容纳所有的 Unicode 字符,所以直接存储 Unicode 码即可,不需要任何编码转换。虽然浪费了空间,但提高了效率。

UTF-8、UTF-16、UTF-32 之间如何转换

前面介绍过,UTF-8、UTF-16、UTF-32 是 Unicode 码表示成不同的二进制格式的编码规则,同样,通过这三种编码的二进制表示,也能获得对应的 Unicode 码,有了字符的 Unicode 码,按照上面介绍的 UTF-8、UTF-16、UTF-32 的编码方法 就能转换成任一种编码了

UTF 字节序

最小编码单元是多字节才会有字节序的问题存在,UTF-8 最小编码单元是一字节,所以 它是没有字节序的问题,UTF-16 最小编码单元是 2 个字节,在解析一个 UTF-16 字符之前,需要知道每个编码单元的字节序

比如:前面提到过,"中" 字的 Unicode 码是 4E2D, "ⵎ" 字符的 Unicode 码是 2D4E, 当我们收到一个 UTF-16 字节流 4E2D 时,计算机如何识别它表示的是字符 "中" 还是 字符 "ⵎ" 呢 ?

所以,对于多字节的编码单元,需要有一个标记显式的告诉计算机,按照什么样的顺序解析字符,也就是字节序,字节序分为 大端字节序 和 小端字节序

小端字节序简写为 LE( Little-Endian ), 表示 低位字节在前,高位字节在后, 高位字节保存在内存的高地址端,而低位字节保存在内存的低地址端

大端字节序简写为 BE( Big-Endian ), 表示 高位字节在前,低位字节在后,高位字节保存在内存的低地址端,低位字节保存在在内存的高地址端

下面以 0x4E2D 为例来说明大端和小端,具体参见下图:

数据是从高位字节到低位字节显示的,这也更符合人们阅读数据的习惯,而内存地址是从低地址向高地址增加

所以,字符0x4E2D 数据的高位字节是 4E,低位字节是 2D

按照大端字节序的高位字节保存内存低地址端的规则,4E 保存到低内存地址 0x10001 上,2D 则保存到高内存地址 0x10002 上

对于小端字节序,则正好相反,数据的高位字节保存到内存的高地址端,低位字节保存到内存低地址端的,所以 4E 保存到高内存地址 0x10002 上,2D 则保存到低内存地址 0x10001 上

BOM

BOM 是 byte-order mark 的缩写,是 "字节序标记" 的意思, 它常被用来当做标识文件是以 UTF-8、UTF-16 或 UTF-32 编码的标记

在 Unicode 编码中有一个叫做 "零宽度非换行空格" 的字符 ( ZERO WIDTH NO-BREAK SPACE ), 用字符 FEFF 来表示

对于 UTF-16 ,如果接收到以 FEFF 开头的字节流, 就表明是大端字节序,如果接收到 FFFE, 就表明字节流 是小端字节序

UTF-8 没有字节序问题,上述字符只是用来标识它是 UTF-8 文件,而不是用来说明字节顺序的。"零宽度非换行空格" 字符 的 UTF-8 编码是 EF BB BF, 所以如果接收到以 EF BB BF 开头的字节流,就知道这是UTF-8 文件

下面的表格列出了不同 UTF 格式的固定文件头

UTF编码

固定文件头

UTF-8

EF BB BF

UTF-16LE

FF FE

UTF-16BE

FE FF

UTF-32LE

FF FE 00 00

UTF-32BE

00 00 FE FF

根据上面的 固定文件头,下面列出了 "中" 字在文件中的存储 ( 包含文件头 )

编码

固定文件头

Unicode 编码

0X004E2D

UTF-8

EF BB BF 4E 2D

UTF-16BE

FE FF 4E 2D

UTF-16LE

FF FE 2D 4E

UTF-32BE

00 00 FE FF 00 00 4E 2D

UTF-32LE

FF FE 00 00 2D 4E 00 00

常见的字符编码的问题

  • Redis 中文key的显示

有时候我们需要向redis中写入含有中文的数据,然后在查看数据,但是会看到一些其他的字符,而不是我们写入的中文

上图中,我们向redis 写入了一个 "中" 字,通过 get 命令查看的时候无法显示我们写入的 "中" 字

这时候加一个 --raw 参数,重新启动 redis-cli 即可,也即 执行 redis-cli --raw 命令启动redis客户端,具体的如下图所示

  • MySQL 中的 utf8 和 utf8mb4

MySQL 中的 "utf8" 实际上不是真正的 UTF-8, "utf8" 只支持每个字符最多 3 个字节, 对于超过 3 个字节的字符就会出错, 而真正的 UTF-8 至少要支持 4 个字节

MySQL 中的 "utf8mb4" 才是真正的 UTF-8


下面以 test 表为例来说明, 表结构如下:

mysql> show create table test\G
*************************** 1. row ***************************Table: test
Create Table: CREATE TABLE `test` (`name` char(32) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

向 test 表分别插入 "中" 字 和 Unicode 码为 0x10A6F 的字符,这个字符需要从 https://unicode-table.com/cn/10A6F/ 直接复制到 MySQL 控制台上,手工输入会无效,具体的执行结果如下图:

从上图可以看出,插入 "中" 字 成功,插入 0x10A6F 字符失败,错误提示无效的字符串,\xF0\X90\XA9\xAF 正是 0x10A6F 字符的 UTF-8 编码,占用 4 个字节, 因为 MySQL 的 utf8 编码最多只支持 3 个字节,所以插入会失败

把 test 表的字符集改成 utf8mb4 , 排序规则 改成 utf8bm4_unicode_ci, 具体如下图所示:

字符集和排序方式修改之后,再次插入 0x10A6F 字符, 结果是成功的,具体执行结果如下图所示

上图中,set names utf8mb4 是为了测试方便,临时修改当前会话的字符集,以便保持和 服务器一致,实际解决这个问题需要修改 my.cnf 配置中 服务器和客户端的字符集

小结

本文从字符编码的历史介绍了 Unicode 出现的原因,接着介绍了 Unicode 字符集中 三种不同的编码方式:UTF-8、UTF-16、UTF-32 以及它们的的编码方法,紧接着介绍了 字节序、BOM ,最后讲到了字符集在 MySQL 和 Redis 应用中常见的问题以及解决方案 ,更多关于 Unicode 的介绍请参考 Unicode 的 RFC 文档


http://chatgpt.dhexx.cn/article/BD0oLyxa.shtml

相关文章

解决eclipse boolean com.ibm.icu.text.UTF16.isSurrogate(char)

window > preferences > java > Editor > mark occurrences and desable : mark occurrences ...

字符集编码 Unicode UTF8 UTF16 UTF32 和BOM(Byte Order Mark)

BOM(Byte Order Mark) 在分析unicode之前,先把bom(byte order mark)说一下。 bom是unicode字符顺序的标识符号,一般以魔数(magic code)的形式出现在以Unicode字符编码的文件的开始的头部,作为该文件的编码标识。 来举个很简单的例子,在windows下新建一个文本文件,并另…

utf8与utf16转换

1.UTF8与UTF16编码转换 std::string ConvertFromUtf16ToUtf8(const std::wstring& wstr) {std::string convertedString;int requiredSize WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, 0, 0, 0, 0);if(requiredSize > 0){std::vector<char> buffer(requ…

探究乱码问题的本源:GBK,UTF8,UTF16,UTF8BOM,ASN1之间的关联

文章目录 前言一、字符集和字符编码的区别和联系二、字符集编码的发展1.单字节2.双字节3.多字节(UNICODE字符集)&#xff08;1&#xff09;UTF-8&#xff08;2&#xff09;UTF-16&#xff08;3&#xff09;UTF-32&#xff08;4&#xff09;UTF BOM 三、不同编码方式的对比分析四…

oracle字符集utf16,oracle字符集AL16UTF16改为ZHS16GBK

AL16UTF16一个汉字占3个字节 ZHS16GBK一个汉字占2个字节 sql> conn / as sysdba; sql> shutdown immediate; database closed. database dismounted. oracle instance shut down. sql> startup mount; oracle instance started. total system global area 135337420 …

字符编码的概念(UTF-8、UTF-16、UTF-32都是什么鬼)

字符集为每个字符分配了一个唯一的编号&#xff0c;通过这个编号就能找到对应的字符。在编程过程中我们经常会使用字符&#xff0c;而使用字符的前提就是把字符放入内存中&#xff0c;毫无疑问&#xff0c;放入内存中的仅仅是字符的编号&#xff0c;而不是真正的字符实体。 这就…

UTF-8 与 UTF-16编码详解

目录 一、UTF-8编码 1、UTF-8介绍 2、UTF-8是如何编码的&#xff1f; 3、上述Unicode码点值范围中十进制值127、2047、65535、2097151这几个临界值是怎么来的呢&#xff1f; 二、UTF-16编码 1、UTF-16介绍 2、UTF-16编码方式 1&#xff09;设计思路 2&#xff09;具体…

了解一下UTF-16

1)先啰嗦一下 UTF-16是一种编码格式。啥是编码格式&#xff1f;就是怎么存储&#xff0c;也就是存储的方式。 存储啥&#xff1f;存二进制数字。为啥要存二进制数字&#xff1f; 因为Unicode字符集里面把二进制数字和字符一一对应了&#xff0c;存二进制数字就相当于存了二进制…

字符编码--UTF-16

2019独角兽企业重金招聘Python工程师标准>>> 第4节 UTF-16 UTF-16是Unicode字符编码五层次模型的第三层&#xff1a;字符编码表&#xff08;Character Encoding Form&#xff0c;也称为"storage format"&#xff09;的一种实现方式。即把Unicode字符集的抽…

蔡勒公式、三角函数

1.蔡勒公式 2.三角函数

蔡勒(Zeller)公式及其推导:快速将任意日期转换为星期数

0. 本文的初衷及蔡勒公式的用处 前一段时间&#xff0c;我在准备北邮计算机考研复试的时候&#xff0c;做了几道与日期计算相关的题目&#xff0c;在这个过程中我接触到了蔡勒公式。先简单的介绍一下蔡勒公式是干什么用的。 我们有时候会遇到这样的问题&#xff1a;看到一个日期…

1185.一周中的几天 四种解法(java),主要新学一下蔡勒公式

题目 给你一个日期&#xff0c;请你设计一个算法来判断它是对应一周中的哪一天。 输入为三个整数&#xff1a;day、month 和 year&#xff0c;分别表示日、月、年。 您返回的结果必须是这几个值中的一个 {“Sunday”, “Monday”, “Tuesday”, “Wednesday”, “Thursday”…

给定日期(年月日)求星期几(蔡勒公式?没那么简单!)

前言 前几日做到一个机试题&#xff0c;给出一个日期&#xff0c;让你输出那天是星期几&#xff0c;这种题无疑两种思路&#xff1a;一是从今天&#xff08;前提是知道今天日期及周几&#xff09;开始推算&#xff0c;计算今天与目标日期差的天数再取模运算&#xff0c;考虑到…

欧拉计划题-19 (蔡勒公式)

欧拉计划题19 前言一 题目描述二 题解分析1.暴力求解&#xff08;低配版解法&#xff09;2.蔡勒公式&#xff08;公式法&#xff09; 三 题解代码 前言 欧拉计划是学习数学、数论选手遨游的海洋&#xff0c;700道题让你我越来越强。 打卡网址链接: link. 一 题目描述 题目链接…

C语言——蔡勒(Zeller)公式:快速将任意日期转换为星期数

蔡勒公式 情景引入公式介绍公式细节代码实现 情景引入 在日常生活中&#xff0c;我们有时候会遇到这样的问题&#xff1a;看到一个日期想知道这一天是星期几。对于这个问题&#xff0c;如果用编程的方式&#xff0c;应该怎么实现呢&#xff1f;你可能已经有思路了&#xff0c;比…

自用笔记58——蔡勒(Zeller)公式

请你编写一个程序来计算两个日期之间隔了多少天。 日期以字符串形式给出&#xff0c;格式为 YYYY-MM-DD&#xff0c;如示例所示。 示例 1&#xff1a; 输入&#xff1a;date1 “2019-06-29”, date2 “2019-06-30” 输出&#xff1a;1 示例 2&#xff1a; 输入&#xff1…

蔡勒公式与Python

蔡勒公式 &#xff08; Zeller formula&#xff09; 作用&#xff1a;从年月日推算星期几 来源&#xff1a;罗马教皇格里高利十三世在1582年组织了一批天文学家&#xff0c;根据哥白尼日心说计算出来的数据&#xff0c;对儒略历作了修改。将1582年10月5日到14日之间的10天宣布…

蔡勒(Zeller)公式理解Get(√)

Preface 偶然做到日期相关题目&#xff0c;了解到Zeller公式。不甘心停留在使用阶段&#xff0c;便想掌握其推导过程。 只适用于格利戈里历法&#xff0c;也就是现今的公历。 1. Zeller公式 标准形式 计算1582年10月4日或之前日期 (罗马教皇决定在1582年10月4日后使用格利戈里…

C语言——蔡勒(Zeller)公式的使用

C语言——蔡勒公式的使用 蔡勒公式简介&#xff1a; 蔡勒&#xff08;Zeller&#xff09;公式&#xff0c;是一个计算星期的公式&#xff0c;随便给一个日期&#xff0c;就能用这个公式推算出是星期几。 计算公式&#xff1a; 核心公式&#xff1a; w(y[y/4][c/4]-2c[26(m1…

c++ operator百样操作符重载(详解)

目录 一、operator &#xff1a;等号判断重载 二、operator &#xff1a; 等号赋值重载 三、operator ! : 不等于重载 四、operator> &#xff1a; 大于号 或者 小于号 重载 五、operator << &#xff1a;输入重定向重载 六、operator &#xff1a;加号重载 …