科普-BOM是什么?和UTF-8什么关系?

摘要:本文讨论了BOM和UTF-8的定义和关系,UTF-8与UTF-16,UTF-32的对比,以及Unicode的定义。


BOM和UTF-8往往被列在一起讨论,他们什么关系?

打开windows的记事本,总是能看到右下角的编码标记。

之所以BOM(字节顺序标记)和 UTF-8 经常被放在一起讨论,但它们的关系其实有点微妙,甚至可以说是"一段历史的误会"。

简单来说,它们的关系可以概括为:

BOM 是 Unicode 标准为 UTF-16 和 UTF-32 编码设计的一个可选标记,用于标识字节序。但在 UTF-8 编码中,它既不必要,也不推荐使用,并且常常带来麻烦。

下面我们来详细解释。

1. 什么是 BOM?

BOM 在一个文件或数据流中有且仅会出现一次,并且必须出现在最开头

  • 全称:Byte Order Mark,字节顺序标记。
  • 本质 :一个特殊的 Unicode 字符 U+FEFF
  • 设计初衷 :用于 UTF-16UTF-32 这类多字节编码。因为这些编码中,一个字符可能由 2个或4个字节表示,这就产生了"哪个字节在前(大头还是小头)"的问题,即字节序(Endianness)。
  • 作用 :文件开头的 BOM 告诉解析器这个文件的字节顺序。
    • 如果文件开头是字节 0xFE 0xFF,表示是大端序(Big-Endian)。
    • 如果文件开头是字节 0xFF 0xFE,表示是小端序(Little-Endian)。

BOM(字节顺序标记)的长度不是固定的,它取决于所使用的 Unicode 编码方案

下面是不同编码下 BOM 的详细位数和字节数:


完整描述

编码方式 BOM(十六进制) 所占字节数 所占位数
UTF-8 EF BB BF 3 字节 24 位
UTF-16 (大端序) FE FF 2 字节 16 位
UTF-16 (小端序) FF FE 2 字节 16 位
UTF-32 (大端序) 00 00 FE FF 4 字节 32 位
UTF-32 (小端序) FF FE 00 00 4 字节 32 位

在这个语境中,它不是 Bill Of Material, 即生产经营管理软件SAP中的用到的概念,它纯属于文件的编码领域内的问题。

2. 什么是 UTF-8?

  • 本质:一种针对 Unicode 的可变长度字符编码。
  • 关键特性 :它使用 1 到 4 个字节来表示一个字符。UTF-8 的字节顺序是固定的,不存在字节序的问题。 无论在哪台计算机上,字节的排列顺序都是一样的。

3. BOM 在 UTF-8 中的角色(和争议)

既然 UTF-8 没有字节序问题,为什么还会有 BOM 呢?

这主要是微软带来的"习惯"。为了区分一个文本文件是 UTF-8 编码还是本地传统的 ANSI 编码(如 GBK、Big5),微软在它的系统(如 Windows 的记事本)中,将 BOM 的概念引入到了 UTF-8。

  • 在 UTF-8 中的 BOM :是字符 U+FEFF 的 UTF-8 编码序列,即三个字节:0xEF 0xBB 0xBF

那么,这个 UTF-8 BOM 是好是坏呢?

优点(非常有限):
  1. 标识文件编码 :如果一个文件以 0xEF 0xBB 0xBF 开头,那么我们可以非常确定这个文件是 UTF-8 编码的。这对于一些无法智能探测编码的旧软件或系统有帮助。
缺点(非常多且严重):
  1. 不符合标准 :Unicode 标准不要求也不推荐在 UTF-8 中使用 BOM。它明确指出,在 UTF-8 中,BOM 的意义仅限于作为编码签名。
  2. 破坏兼容性 :这是最致命的问题。很多在 Unix/Linux 环境下设计的软件(如脚本解释器、编译器、服务器软件)不期望也不识别 UTF-8 BOM。
    • 对于 Shell 脚本/Python/PHP 等 :文件开头的 #!(Shebang)行如果前面有 BOM,会导致解释器找不到,从而执行失败。
    • 对于 Web :在 PHP 文件中,如果开头有 BOM,它会在服务器发送 HTTP 头之前被输出,可能导致 Cannot modify header information 错误。在 HTML/CSS/JS 文件中,BOM 可能导致页面布局错乱或脚本错误。
  3. 多余 :现代软件和操作系统有更智能的方法来探测文本编码(如统计分析法、charset 声明),不再需要依赖 BOM。
  4. 被视为内容 :对于不识别 BOM 的软件,这三个字节 0xEF 0xBB 0xBF 会被当作文件内容的一部分处理,可能显示为一个不可见的零宽度空格(ZWNBSP)或一个乱码字符(如 "ï>>¿")。

总结关系

特性 UTF-16/UTF-32 UTF-8
BOM 的作用 必要或重要,用于标识字节序。 不必要且有害,用于(不推荐的)编码签名。
BOM 的值 0xFEFF0xFFFE(作为2或4字节) 0xEF 0xBB 0xBF(三个字节)
业界实践 普遍使用。 强烈不推荐在 Unix/Linux 和 Web 领域使用。Windows 部分软件(如记事本)是主要来源。

建议

  • 在跨平台项目(Web、Linux脚本、源代码)中绝对不要使用带 BOM 的 UTF-8。确保你的代码编辑器/IDE 设置为保存为 "UTF-8 without BOM"。
  • 仅在纯 Windows 环境中:如果与你交互的所有软件都明确支持并期望 BOM(例如某些旧的 Windows-only 应用),可以考虑使用。但即使是 Windows 上的现代开发(如 .NET Core, VS Code),也默认使用无 BOM 的 UTF-8。

所以,BOM 和 UTF-8 被列在一起,更像是在讨论 "一个不应该存在的组合所引发的一系列问题"

附1 UTF-8与UTF-16,UTF-32的对比

UTF-8、UTF-16 和 UTF-32 都是 Unicode 标准的字符编码方案,它们的核心区别在于 如何将 Unicode 码点(一个唯一的数字)转换成一串字节

我们从几个维度来详细比较它们的区别:


核心区别一览表

特性 UTF-8 UTF-16 UTF-32
编码单位 8位(1字节) 16位(2字节) 32位(4字节)
最小字节数 1字节 2字节 4字节
最大字节数 4字节 4字节 4字节
字节序 无字节序问题 有字节序问题(需要BOM) 有字节序问题(需要BOM)
与ASCII兼容性 完全兼容 不兼容 不兼容
空间效率 (对英文/西欧语) (对英文效率低,对中文尚可) 极低(浪费空间)
处理效率 需要解析(变长) 需要解析(基本是定长,但不完全是) 极高(直接定长)
BOM使用 不必要且不推荐 推荐使用 推荐使用

详细解释

1. UTF-8(8-bit Unicode Transformation Format)
  • 工作原理 :它是一种变长 编码,使用 1 到 4 个字节来表示一个字符。
    • ASCII 字符(U+0000 到 U+007F)用 1个字节 表示,与 ASCII 编码完全一致。
    • 其他字符根据需要,使用 2个、3个或4个字节。
  • 优点
    • 兼容ASCII:这是它最大的优势。纯ASCII文件本身就是合法的UTF-8文件。
    • 无字节序问题:因为是单字节序列,不存在"哪个字节在前"的问题,跨平台交换数据非常方便。
    • 空间高效:对于英文和西欧语言文本,它比UTF-16节省大量空间。
  • 缺点
    • 处理效率稍低:因为字符的字节数不固定,程序需要解析字节序列才能确定一个完整的字符,理论上比定长编码慢一些(但现代CPU优化得很好,差距很小)。
  • 应用场景互联网、操作系统、文件存储的绝对主流。是当今事实上的标准。Linux、HTML、JSON、XML等都推荐或强制使用UTF-8。
2. UTF-16(16-bit Unicode Transformation Format)
  • 工作原理 :它基本是定长 的,但也不完全是。它使用 2个或4个字节 来表示一个字符。
    • 基本多文种平面(BMP, U+0000 到 U+FFFF)的字符用 2个字节 表示。
    • 辅助平面(如表情符号、生僻汉字)的字符用 4个字节 表示(通过"代理对"机制)。
  • 优点
    • 空间效率折中:对于大量使用BMP内字符的文本(如中文、日文),它比UTF-8效率高或相当(一个常用汉字在UTF-8占3字节,在UTF-16占2字节)。
    • 处理效率尚可:大部分常用字符都是2字节,处理起来比UTF-8简单。
  • 缺点
    • 有字节序问题:2字节或4字节的单元需要定义字节序(大端或小端),所以需要BOM来标识。
    • 不兼容ASCII :一个ASCII字符在UTF-16中也被存储为2个字节,其中高字节是0x00,这在与旧C语言函数(以null字节判断字符串结束)交互时可能有问题。
  • 应用场景Windows操作系统内部、Java和JavaScript(ECMAScript)语言内部、.NET平台
3. UTF-32(32-bit Unicode Transformation Format)
  • 工作原理 :它是纯粹的定长 编码。每一个 Unicode 码点都用 exactly 4个字节 来表示。
  • 优点
    • 处理效率最高:因为每个字符都是固定的4字节,通过索引随机访问字符串中的第N个字符是O(1)操作,非常简单快速。
  • 缺点
    • 空间效率极低:极度浪费空间。一个简单的英文字母'A'也需要4个字节存储,是UTF-8的4倍,是UTF-16的2倍。
    • 有字节序问题:同样需要BOM来标识。
  • 应用场景 :主要用于内存处理,当程序需要频繁随机访问和操作单个字符时。很少用于文件存储或网络传输。

举例说明

以汉字" "为例,它的 Unicode 码点是 U+4E2D。更详细的过程可以看下文的附录。

  • UTF-8 :码点 4E2D 落在 UTF-8 三字节的编码范围内。经过编码规则转换,得到3个字节:0xE4 0xB8 0xAD
  • UTF-16 :码点 4E2D 在BMP内,所以直接使用2个字节表示。根据字节序不同,可能是:
    • 大端序:0x4E 0x2D
    • 小端序:0x2D 0x4E
  • UTF-32 :码点 4E2D 直接扩展为4个字节。根据字节序不同,可能是:
    • 大端序:0x00 0x00 0x4E 0x2D
    • 小端序:0x2D 0x4E 0x00 0x00

而以英文字母"A "为例,码点是 U+0041

  • UTF-80x41(1个字节,与ASCII完全相同)
  • UTF-160x00 0x41(大端序,2个字节)
  • UTF-320x00 0x00 0x00 0x41(大端序,4个字节)

总结与选择

  • 选择 UTF-8几乎总是正确的选择。除非你有非常特殊的、必须使用其他编码的理由(比如与特定的Windows API交互),否则在存储、传输和一般性处理时,都应优先使用UTF-8。它是互联网的通用语言。
  • 理解 UTF-16:主要是因为它在Windows、Java和JavaScript中很常见,你需要知道它的存在和特性,以便与这些平台交互。
  • 了解 UTF-32:你知道它主要用于对性能要求极高的内部字符串处理即可,日常开发中很少主动使用。

BOM(字节顺序标记)的长度不是固定的,它取决于所使用的 Unicode 编码方案

下面是不同编码下 BOM 的详细位数和字节数:


总结表

编码方式 BOM(十六进制) 所占字节数 所占位数
UTF-8 EF BB BF 3 字节 24 位
UTF-16 (大端序) FE FF 2 字节 16 位
UTF-16 (小端序) FF FE 2 字节 16 位
UTF-32 (大端序) 00 00 FE FF 4 字节 32 位
UTF-32 (小端序) FF FE 00 00 4 字节 32 位

附2 中字对应的编码

UTF-8对中的编码

下面显示 UTF-8 是如何精确地将 Unicode 码点 U+4E2D(汉字"中")编码成三个字节 0xE4 0xB8 0xAD 的。

这个过程就像一个精密的编码协议。


第一步:找到字符的 Unicode 码点

字符:

  • Unicode 码点U+4E2D
  • 码点的二进制表示0100 1110 0010 1101
    • (这里 4E0100 11102D0010 1101

第二步:确定 UTF-8 编码模板

UTF-8 是一种变长编码,它根据码点值的大小范围,选择不同的编码模板:

码点范围(Unicode) 码点位数 UTF-8 编码模板(二进制) 所需字节数
U+0000 - U+007F 7 bits 0xxxxxxx 1 byte
U+0080 - U+07FF 11 bits 110xxxxx 10xxxxxx 2 bytes
U+0800 - U+FFFF 16 bits 1110xxxx 10xxxxxx 10xxxxxx 3 bytes
U+10000 - U+10FFFF 21 bits 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 4 bytes

我们的码点 U+4E2D 落在 U+0800 - U+FFFF 范围内,所以我们将使用 3字节模板1110xxxx 10xxxxxx 10xxxxxx

这个模板中的 x 就是我们要填充的码点比特位。

第三步:将码点的比特位填入模板

  1. 获取码点的有效比特位U+4E2D 的二进制是 0100 1110 0010 1101。这是一个16位的数,但对我们来说,只需要从最低位开始数起,填满模板即可。

  2. 对齐模板 : 3字节模板为我们提供了 4 + 6 + 6 = 16x 的位置,正好容纳一个16位的码点。

    我们把模板和位置画出来:

    markdown 复制代码
    UTF-8 模板 (3字节): 1110xxxx 10xxxxxx 10xxxxxx
                        位 15-12   位 11-6   位 5-0
  3. 填入比特位 : 将码点 0100 1110 0010 1101 的比特位,从高位到低位,依次填入 x 的位置。

    • 第一个字节 1110xxxx :填入码点的最高4位 0100。 -> 1110 0100 -> 二进制 11100100 -> 十六进制 0xE4

    • 第二个字节 10xxxxxx :填入码点的中间6位 111000。 -> 10 111000 -> 二进制 10111000 -> 十六进制 0xB8

    • 第三个字节 10xxxxxx :填入码点的最低6位 101101。 -> 10 101101 -> 二进制 10101101 -> 十六进制 0xAD

最终结果

经过以上步骤,我们得到了最终的 UTF-8 编码:

步骤 结果(二进制) 结果(十六进制)
原始码点 0100 1110 0010 1101 U+4E2D
填入模板后 11100100 10111000 10101101 0xE4 0xB8 0xAD

所以,汉字" "的 UTF-8 编码就是连续的三个字节:E4 B8 AD


可视化总结

这个填充过程可以直观地理解为:

ini 复制代码
Unicode 码点 (16 bits):    0100   111000   101101
                           /       |         \
UTF-8 编码模板:         [1110xxxx] [10xxxxxx] [10xxxxxx]
填充后:                [11100100] [10111000] [10101101]
十六进制:                 E4        B8         AD

这就是为什么会在文件或网络数据包中看到"中"字以 E4 B8 AD 这三个字节的形式存在。 任何支持 UTF-8 的解码器在读到以 1110 开头的字节时,就知道必须再读取两个以 10 开头的字节,并将这三字节中特定的比特位提取出来,还原出原始的 Unicode 码点 U+4E2D

UTF-16对中的编码

我们把"中"字 (U+4E2D) 在 UTF-16 下的情况再拆解一遍。


1. Unicode 码点本身

字符:

  • Unicode 码点U+4E2D
  • 码点的二进制表示0100 1110 0010 1101
    • 这只是一个逻辑上的数字,还没有被编码成字节。

2. UTF-16 编码的第一步

UTF-16 将这个码点直接映射为一个 16位(2字节)的单元

这个16位单元的值就是码点本身:

  • 16位单元0100 1110 0010 1101

问题来了:这个16位的单元,在存储时要拆成2个字节,哪个字节在前?


3. 字节序的二进制对决

我们把16位单元 0100 1110 0010 1101 拆成两个字节:

  • 高字节 (Most Significant Byte - MSB)0100 1110 (对应十六进制 0x4E
  • 低字节 (Least Significant Byte - LSB)0010 1101 (对应十六进制 0x2D

现在看两种顺序:

场景一:大端序

规则:高字节在前,低字节在后。 符合人类阅读数字的习惯(百位、十位、个位)。

  • 字节序列[高字节] [低字节]
  • 二进制流0100 1110 0010 1101
  • 十六进制4E 2D
  • 解读 :当计算机读取这两个字节时,它会将第一个字节作为高位,直接组合成 0x4E2D,这与原始码点完全一致。
场景二:小端序

规则:低字节在前,高字节在后。 符合某些CPU(如x86架构)处理数据的习惯。

  • 字节序列[低字节] [高字节]
  • 二进制流0010 1101 0100 1110
  • 十六进制2D 4E
  • 解读 :当计算机读取这两个字节时,它会将第一个字节作为低位,第二个字节作为高位,组合成 0x2D4E?不!计算机会根据"小端序"规则,在内存中进行字节交换 ,将其解释0x4E2D

关键点: 在小端序存储中,磁盘上或网络上传的物理字节顺序 确实是 2D 4E,但CPU在加载它时,会透明地将其转换回逻辑上的 4E 2D 来使用。如果一台大端序的机器不经过转换直接读取这个 2D 4E 流,就会错误地认为码点是 U+2D4E(这是一个不同的字符,'-.')。


总结对比表

大端序 小端序
逻辑码点 U+4E2D U+4E2D
16位单元 0100 1110 0010 1101 0100 1110 0010 1101
物理字节序列 0100 1110 0010 1101 (0x4E 0x2D) 0010 1101 0100 1110 (0x2D 0x4E)
文件/网络中的Hex 4E 2D 2D 4E
如果没有BOM... 另一台小端序机器会误读为 U+2D4E 另一台大端序机器会误读为 U+2D4E

结论: 正是因为这个16位的单元在存储时内部的字节顺序会变化 ,才产生了字节序问题,也才需要 BOM (0xFEFF) 来明确标识这个顺序,确保所有系统都能正确解读。而 UTF-8 的每个字节都是独立的,不存在这种"单元内部的顺序"问题。

附3 大端序和小端序的分裂

为什么会存在大端序和小端序的区别?归根结底,大端序和小端序的区别本质上是历史原因,是计算机早期发展过程中不同厂商设计CPU架构时做出的不同选择,可以看作是"技术世袭"的结果。

这就像不同地区的人们决定靠左行驶还是靠右行驶一样,最初都有各自的理由,但一旦形成标准就难以更改。


1. 两种思维的起源

这两种顺序源于对人类思维和硬件电路的不同理解和优化。

大端序:人类的直觉
  • 理念最高位字节存储在最小的地址上。这和我们书写数字、阅读数字的习惯完全一致。
  • 例子 :数字 0x12345678 在内存中(从低地址到高地址)的存储顺序就是 12 34 56 78
  • 绰号"大头",因为最重要的部分(头)在前面。
  • 早期代表Motorola 68000 系列(用于早期的苹果Macintosh、Amiga等)、Sun SPARCIBM POWER 。许多网络协议(如TCP/IP)也规定使用大端序作为"网络字节序",这确保了数据在不同机器间传输时有统一标准。

为什么选择大端序?

  • 直观易懂:内存转储时,字节顺序和书写顺序一致,便于调试。
  • 容易判断正负:第一个字节的最高位就决定了整个数的正负号。
  • 字符串比较友好:对于同样是大端序存储的ASCII字符串,可以用内存比较函数直接比较数值大小。
小端序:硬件的效率
  • 理念最低位字节存储在最小的地址上。这更像是把数字的"个位"放在最前面。
  • 例子 :数字 0x12345678 在内存中(从低地址到高地址)的存储顺序是 78 56 34 12
  • 绰号"小头",因为最不重要的部分(尾巴)在前面。
  • 现代代表Intel x86 及其兼容架构(包括我们今天的PC和服务器CPU)、AMDARM(默认是小端模式,但可配置)。

为什么选择小端序?

  1. 数学运算更方便:计算机做加法、乘法都是从最低位开始的。如果数字用小端序存储,CPU在读取第一个字节时就可以立刻开始计算,而不需要先知道整个数字有多长或者等待所有高位字节加载完毕。这对于早期处理能力有限的CPU至关重要。
  2. 数据类型转换灵活 :一个32位整数 0x00001234 在小端序中存储为 34 12 00 00。如果你将其作为16位整数来读取(只读前两个字节),你会直接得到 0x1234,转换是无缝的。而在大端序中,32位存储为 00 00 12 34,取前两个字节得到的是 0x0000,这是错误的。

2. 历史的延续与现状

  • Intel 的胜利:随着基于 Intel x86 架构的 IBM PC 及其兼容机在个人电脑领域的绝对成功,小端序被广泛传播和继承下来。
  • ARM 的统治 :在移动端和嵌入式领域,占据统治地位的 ARM 处理器也主要使用小端序。这使得小端序成为了当今事实上的主流
  • 领域的割据
    • CPU/内存小端序为主
    • 网络传输强制大端序(网络字节序)。所有数据在发送前都要被转换成大端序,接收方再转换回自己的本地字节序。这保证了异构系统之间的通信。
    • 文件格式取决于标准。很多格式(如PNG)明确规定使用大端序,而有些格式则依赖于创建它的平台。

总结

所以,字节序的区别确实是一个历史遗留问题,其根源在于:

  1. 设计哲学的差异 :大端序迎合人类直觉 ,小端序优化硬件计算
  2. 早期巨头的选择:Motorola vs. Intel,两大阵营的不同选择导致了分裂。
  3. 路径依赖:一旦一个生态系统(如Windows/PC)建立在某种字节序上,其上的所有软件、工具链都会围绕它来构建,改变的成本高到无法承受。

这就造成了我们今天必须面对的现实:在一个由小端序CPU主导的世界里,处理来自网络(大端序)或不同来源文件的数据时,程序员依然需要时刻保持对字节序的警惕。UTF-8 的成功,很大程度上正是因为它巧妙地避开了这个历史遗留的"坑"。

相关推荐
小年糕是糕手3 小时前
【数据结构】常见的排序算法 -- 插入排序
c语言·开发语言·数据结构·学习·算法·leetcode·排序算法
墨染点香4 小时前
LeetCode 刷题【142. 环形链表 II】
算法·leetcode·链表
海琴烟Sunshine4 小时前
leetcode 263. 丑数 python
python·算法·leetcode
信仰_2739932434 小时前
Guava Cache淘汰算法
算法·guava
散峰而望4 小时前
C++入门(二) (算法竞赛)
开发语言·c++·算法·github
Cx330❀4 小时前
《C++ 搜索二叉树》深入理解 C++ 搜索二叉树:特性、实现与应用
java·开发语言·数据结构·c++·算法·面试
不染尘.5 小时前
2025_11_5_刷题
开发语言·c++·vscode·算法·贪心算法·动态规划
2501_929177585 小时前
C++中的虚基类
开发语言·c++·算法
Blossom.1186 小时前
把AI“贴”进路灯柱:1KB决策树让老旧路灯自己报「灯头松动」
java·人工智能·python·深度学习·算法·决策树·机器学习