LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

字符编码:从基础到乱码解决

freeflydom
2025年3月14日 9:46 本文热度 138

笔者尝试通过梳理字符编码的核心原理,同时简单的介绍一下常见标准,希望能够帮助各位读者构建对字符编码技术的基础认知框架。

此外本文所述均只在 Windows 下实验。

问题的引入#

在日常开发中,当我们尝试将中文输出到控制台时,点击编译。这时,细心的读者可能会关注到 VS 的控制台会输出一段这样的警告(也有可能是团队规定不允许有警告出现🌝):

文件包含在偏移 0x9c8 处开始的字符,该字符在当前源字符集中无效(代码页 65001)。

同时你心心念念的中文,输出到控制台却成为了乱码。为什么会出现这种问题呢?

这一系列的问题,归根结底,就是一个字符在计算机中,应该怎么样来表示。也就是字符的编码问题。所以,让我们先来了解了解,现代计算机体系中的编码模型是什么样的。

这一系列问题,追根溯源,其实就是一个字符在计算机中该如何表示的问题,即字符的编码问题。那么,我们先来了解一下现代计算机体系中的编码模型是怎样的。

字符编码模型#

Unicode 字符编码结构模型分为 5 层,下面我们以一个“汉”字为例,为大家介绍这 5 层。

抽象字符集 (Abstract Character Set) ACR#

待编码字符集,定义字符的逻辑集合,不涉及具体的编码逻辑。这一层仅确定“汉”字属于某个字符集。(像 GB2312 就只收录了 6763 个常用的汉字和字符,一些生僻字就没有被收录进来。又比如 ASCII 中就没有中文字符。)

编码字符集 (Coding Character Set) CCS

从抽象字符集(ACR)映射到一组非负整数,也就是为每一个字符分配一个唯一的二数字(码位/码点)。例如:Unicode、ASCII、USC、GBK等编码。

在 Unicode 中,“汉”,表示成:\u6C49,而在 GBK 中,“汉”,表示成:0xBABA。

字符编码表 (Character Encoding Form) CEF

一个从一组非负整数(来自 CCS)到一组特定宽度代码单元序列的映射。我们常说的 UTF-8、UTF-16、UTF-32 就是一个字符编码表。他规定了在抽象字符集中的“非负整数”怎么用字节表示。

例如在 UTF-8 中,“汉”字用三个字节表示:0xE6B189。

字符编码方案 (Character Encoding Scheme) CES

一个从一组代码单元序列(来自一个或多个 CEF)到序列化字节序列的映射。

定义码元序列的存储方式,解决字节序等问题:

例如:

  • UTF-8无需处理字节序(单字节码元),直接存储为 0xE6 0xB1 0x89

  • UTF-16若使用大端序(Big-Endian),则存储为 FE FF 6C 49(前两个字节为BOM标识)。

此层确保不同系统对同一编码单元序列的解析一致性。

传输编码语法 (Transfer Encoding Syntax) TES

针对特殊场景的二次编码,如网络传输:

  • 通过Base64将二进制 0xE6B189 转换为字符串“5rGJ”

  • URL编码将UTF-8字节转换为 %E6%B1%89

通过上面的介绍,相信你对现代编码模型的五层有了基本的了解。感兴趣的读者可以去看 Unicode technical report #17

讲完了字符编码模型,接下来我们来了解一些常见的字符编码标准及其特点。

常见字符编码 

相信大家在日常的开发中,经常听到 Unicode、GB2312、GBK、UTF-8、UTF-16、UTF-32、ANSI,却又对这些概念比较模糊。首先要明确一点的是,Unicode、GB2312、GBK 都是编码字符集,而UTF-8、UTF-16、UTF-32 则是 Unicode 的编码字符表。ANSI 比较特殊,我们待会再具体介绍。

由于篇幅限制,对各个编码的具体编码模式感兴趣的读者可以在参考文献中自行了解。

ASCII#

引用自ASCII-WikipediaASCII-simple-Wikipedia

ASCII,全称American Standard Code for Information Interchange(美国信息交换标准代码),于 1963 年发布。标准 ASCII 采用 7 位二进制数来表示字符,因此它最多只能表示 128 个字符。


ASCII 编码虽然解决了英语的编码问题,但中文怎么办呢?汉字有那么多字。此时,就有了 GK2312 编码。

GB2312

引用自ASCII-Wikipedia-zhASCII-Wikipedia-en

GB2312,又称 GB/T 2312-1980,全称信息交换用汉字编码字符集·基本集》,与 1980 年由中国国家标准总局发布。GB2312 收录共收录 6763 个汉字,其中一级汉字3755个,二级汉字3008个;同时收录了包括拉丁字母希腊字母日文平假名片假名字母、注音符号俄语西里尔字母在内的682个字符。

GB2312 使用两个字节来表示,第一个字节称为“高位字节”,对应分区的编号(把区位码的“区码”加上特定值);第二个字节称为“低位字节”,对应区段内的个别码位(把区位码的“位码”加上特定值)。

Unicode

随着计算机技术在全世界的广泛应用,越来越多来自不同地区,拥有不同文字的人们也加入了计算机世界,同时也带来了越来越多的种类。在 1991 年,由一个非盈利机构 Unicode 联盟首次发布了 The Unicode Standard,旨在统一整个计算机世界的编码。

Unicode 的编码空间从 U+0000 到 U+10FFFF,划分为 17 个平面(plane),每个平面包含216 个码位(0x0000~0xFFFF),其中第一个平面称为基本多语言平面(Basic Multilingual Plane,BMP),其他平面称为辅助平面(Supplementary Planes)。

具体编码方式可以参考:彻底弄懂 Unicode 编码

GBK

由于 GB2312 只收录了 6763 个汉字,有一些 GB2312 推出之后才简化的汉字,部分人用名字、繁体字等未被收录进标准,由中华人民共和国全国信息技术标准化技术委员会1995年12月1日制订了 GBK 编码。GBK 共收录 21886 个汉字和图形符号。

UTF-8、UTF-16、UTF-32

Unicode 转换格式(Unicode Transformation Format,简称 UTF),一个字符的 Unicode 编码虽然是确定的,但是由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对 Unicode 编码的实现方式有所不同。所以就有着不同的 Unicode 转换格式:UTF-8、UTF-16、UTF-32。

UTF-8

UTF-8(8-bit Unicode Transformation Format)是一种用于实现Unicode的编码方式,它使用一到四个字节来表示一个字符。UTF-8具有良好的兼容性和效率,能够与ASCII字符集完全兼容,对于其他语言字符也能够以较高效的方式进行编码。

UTF-8 采用下面的规则来编码

  • 在ASCII码的范围,用一个字节表示,超出ASCII码的范围就用字节表示,这就形成了我们上面看到的UTF-8的表示方法,这样的好处是当UNICODE文件中只有ASCII码时,存储的文件都为一个字节,所以就是普通的ASCII文件无异,读取的时候也是如此,所以能与以前的ASCII文件兼容。

  • 大于ASCII码的,就会由上面的第一字节的前几位表示该unicode字符的长度,比如110xxxxx前三位的二进制表示告诉我们这是个2BYTE的UNICODE字符;1110xxxx是个三位的UNICODE字符,依此类推;xxx的位置由字符编码数的二进制表示的位填入。越靠右的x具有越少的特殊意义。只用最短的那个足够表达一个字符编码数的多字节串。注意在多字节串中,第一个字节的开头"1"的数目就是整个串中字节的数目。

码点的位数码点起值码点终值字节序列Byte 1Byte 2Byte 3Byte 4Byte 5Byte 6
7U+0000U+007F10xxxxxxx




11U+0080U+07FF2110xxxxx10xxxxxx



16U+0800U+FFFF31110xxxx10xxxxxx10xxxxxx


21U+10000U+1FFFFF411110xxx10xxxxxx10xxxxxx10xxxxxx

26U+200000U+3FFFFFF5111110xx10xxxxxx10xxxxxx10xxxxxx10xxxxxx
31U+4000000U+7FFFFFFF61111110x10xxxxxx10xxxxxx10xxxxxx10xxxxxx10xxxxxx

UTF-8 BOM

BOM,全称字节序标志(byte-order mark)。目的是为了表示 Unicode 编码的字节顺序。使用 BOM 模式会在文件头处添加 U+FEFF,对应到 UTF-8 格式的文件,则会在文件起始处添加三个字节:0xEF、0xBB、0xBF。 还记得我们之前在说字符编码方案时,说过 UTF-8 无需处理大端小端。那为什么不需要呢?

字节序(Endianness)是指多字节数据(如一个整数或一个字符的多字节表示)在内存中的存储顺序。而对于 UTF-8 中,每个使用UTF-8存储的字符,除了第一个字节外,其余字节的头两个比特都是以"10"开始,除了第一个字符以外,其他都是唯一的。

但是 Unicode 标准并不要求也不推荐使用 BOM 来表示 UTF-8,但是某些软件如果第一个字符不是 BOM (或者文件里只包含 ASCII),则拒绝正确解释 UTF-8

UTF-16

UTF-16 把 Unicode 字符集的抽象码位映射为 16 位长的整数(即码元)的序列,也就是说在 UTF-16 编码方式下,一个 Unicode 字符,需要一个或者两个 16 位长的码元来表示。因此 UTF-16 也是一种具体编码。

Unicode 的基本多语言平面(BMP)内,从U+D800到U+DFFF之间的码位区段是永久保留不映射到Unicode字符。UTF-16就利用保留下来的0xD800-0xDFFF区块的码位来对辅助平面的字符的码位进行编码。

UTF-16 采用下面的方法用来编码:

  • 基本平面的码点,直接用 16 比特长的单个码元表示,数值等价于对应的码位。

  • 辅助平面的码点,先将码位减去 0x10000,得到的值范围为 20 比特长的 0x00000 ~ 0xFFFFF。其次高位的 10bit(值范围为 0x000 ~ 0x3FF),加上 0xD800,得到第一个码元,又称高位代理(现代 Unicode 标准称之为前导代理),值范围为 0xD800 ~ 0xDBFF。再将低位的 10bit(值范围也为 0x000 ~ 0x3FF),加上 0xDC00,得到第二个码元,又称低位代理(现代 Unicode 标准称之为后尾代理),值范围为 0xDC00 ~ 0xDFFF

同样我们也以“汉”字为例,它在 Unicode 中为:U+6C49,处于 BMP 中,所以直接用 0x6C49 表示。而另外一个以U+10437编码(𐐷)为例:

  1. 0x10437 减去 0x10000,结果为0x00437,二进制为 0000 0000 0100 0011 0111

  2. 分割它的上10位值和下10位值(使用二进制):0000 0000 01  00 0011 0111

  3. 添加 0xD800 到上值,以形成高位0xD800 + 0x0001 = 0xD801

  4. 添加 0xDC00 到下值,以形成低位0xDC00 + 0x0037 = 0xDC37

UTF-32#

Unicode-32 直接采用 4 个字节来存储 Unicode 码位。这种编码格式的优点是能够直接用 Unicode 码位来索引,但同时,相比于其他编码(UTF-8、UTF-16),浪费空间,所以应用并不广泛。

ANSI#

当我们创建一个文本文件,并用 Notepad++查看其默认编码时,会看到一个 ANSI

那么 ANSI 是什么编码呢?简而言之,ANSI 不是某一种特定的字符编码,而是在不同系统中,表示不同的编码。

输入字符集与执行字符集

  • 输入字符集:决定了编译器如何读取和解析源代码中的字符。

  • 执行字符集:决定了编译器如何将字符和字符串常量编码并存储到可执行文件中。

例如:输入字符集为GB2312时,"中文"两个字,对应的二进制是:

而输入字符集为UTF-8时则为下面:

而执行字符集,可以通过显示设置字符集来修改:

在编译器中显式设置输入字符集和执行字符集。对于GCC编译器,可以使用 -finput-charset=UTF-8 -fexec-charset=UTF-8 选项;对于MSVC编译器,可以使用 /source-charset:utf-8 /execution-charset:utf-8 选项,你也可以使用 /utf-8来指定输入字符集和执行字符集都为 UTF-8。

如果输入字符集和执行字符集不一致,编译器需要在编译过程中进行字符编码的转换。当两者不一致时,编译器需进行编码转换,可能引发:

  • 字符映射丢失(如GBK→ASCII)

  • 字节序列错误(如UTF-8→UTF-16LE)

所以,尽量将两个字符集设置成一样的。

代码页

在计算机发展的早期阶段,ASCII编码(美国信息交换标准代码)是主流的字符编码方式,它使用7位二进制数表示128个字符,包括英文字母、数字和一些标点符号。然而,ASCII编码无法满足多语言环境的需求,因为世界上有成千上万种语言和符号。

为了解决这个问题,操作系统和软件开发商引入了代码页的概念。代码页允许系统支持多种字符集,尤其是那些超出ASCII范围的语言字符。在Windows操作系统中,代码页是系统用来处理文本数据的机制。例如,当用户在系统中输入或显示文本时,系统会根据当前的代码页设置来解释这些字符。

假设你有一个文本文件,内容是中文字符“你好”。如果这个文件是用GBK编码保存的,那么它的字节序列可能是 C4 E3 BA C3。操作系统会根据代码页936(GBK)来解释这些字节,并正确显示为“你好”。但如果系统错误地使用了代码页1252(西欧字符集),这些字节会被解释为乱码,因为代码页1252中没有对应的字符。

再探乱码

看到这里,相信各位读者对字符编码已经有些一些基础的了解。所以,下面让我们尝试解答刚开始提出的问题:

  1. 为什么 std::cout << "中文" << std::endl; 输出到控制台会乱码?

  2. 该字符在当前源字符集中无效(代码页65001)

为什么控制台会输出乱码?

假设有这样一段代码:

// main.cpp
#include <iostream>
int main(int argc, char** argv){
    std::cout << "中文" << std::endl;
    return 0;
}

运行起来后,会发现输出到控制台是这种情况:

这个问题的影响因素有两个:

  • 控制台字符编码

  • 文件源字符集

首先,在 Windows 下,控制台的默认编码是当前系统的代码页(通常是 GB2312),所以如果你输出到控制台的字符不是当前代码页编码对应的字符,那么就会发生乱码。当前系统的代码页通过 cmd 执行命令 chcp来查看。 假如文件的源格式是 UTF-8,那么"中文"这两个字的字节序列为:

当我们输出到控制台时,按照 GB2312 编码去解析这 6 个字节时,我们会得到:

涓(E4B8)(ADE6)枃(9687),其中 ADE6 在 GB2312 中为错误编码,所以会显示一个问号。

根据这个思路,我们有两种方法解决这个问题:

  • 修改控制台字符编码

  • 修改源文件字符集

第一种我们通过执行 chcp 来修改当前代码页:

// main.cpp
#include <iostream>
int main(int argc, char** argv){
    // 65001 代表UTF-8
    system("chcp 65001");
    std::cout << "中文" << std::endl;
    return 0;
}

第二种,就是修改文件的字符编码格式,改成 GB2312。怎么改我就不赘述了,网上一大把。

该字符在当前源字符集中无效?

这一个问题与输入字符集有关,当文件编码与编译器预期不一致,例如你的文件是GB2312编码,但编译器(如MSVC)默认使用UTF-8(代码页65001)来解析源文件。GB2312和UTF-8是不兼容的编码格式,导致编译器无法正确解析文件中的字符。

笔者的 Visual Studio 工程命令行有一个 /utf-8,也就代表输入、执行编码集都为 utf-8。所以,当你文件的编码为 GB2312 时,

  1. “创”字的GB2312编码在GB2312编码中,“创”字的编码是 0xD4 0xB4

  2. “创”字的UTF-8编码在UTF-8编码中,“创”字的编码是 0xE5 0x8D 0x94

  3. 当编译器以UTF-8编码解析文件时,会将 GB2312编码的字节序列 0xD4 0xB4 视为一个潜在的UTF-8字符。然而,根据UTF-8的编码规则: 0xD4 是一个以 1101 开头的字节,表示这是一个两字节字符,第一个字节的格式应为 110xxxxx ,第二个字节的格式应为 10xxxxxx 。但是, 0xD4 的二进制是 11010100 ,而 0xB4 的二进制是 10110100 。

虽然第二个字节符合 10xxxxxx 的格式,但第一个字节的值 0xD4 超出了UTF-8两字节字符的合法范围( 0xC0 到 0xDF ),因此整个字节序列 0xD4 0xB4 是无效的UTF-8字符。

QString 一些字符相关的函数

在 QString 中有许多的转换函数:

  1. QString::fromLatin1

  2. QString::fromLocal8Bit

  3. QString::fromUtf8

  4. QString::fromWCharArray

QString 是以 UTF-16 的格式存储的字符:

QString stores a string of 16-bit QChars, where each QChar corresponds to one UTF-16 code unit.

所以,调用上面这些函数就是用指定的格式读取字符,并将这些字符转换成 UTF-16 格式。参看下面的例子:

    QString str("中文");
    qDebug() << str;
    qDebug() << QStringLiteral("2中文");
    qDebug() << QString::fromLatin1("3中文");             // Latin-1 ≈ ASCII
    qDebug() << QString::fromLocal8Bit("4中文");          // Windows下取决于当前代码页 一般中文系统是:GBK
    qDebug() << QString::fromUtf8("5中文");               // UTF-8
    qDebug() << QString::fromWCharArray(L"6中文");        // Returns a copy of the string, where the encoding of string depends on the size of wchar. 
                                                          // If wchar is 4 bytes, the string is interpreted as UCS-4,
                                                          // if wchar is 2 bytes it is interpreted as UTF-16.

输入字符集为GB2312时:

输入字符集为UTF-8时:

最后的最后#

感谢各位读者阅读本博客,本博客内容在创作过程中,参考了大量百科知识以及其他优秀博客,并结合笔者自身在实际工作中遇到的相关问题。笔者希望通过这篇博客,能为各位读者在字符编码这一块提供一些有价值的见解和帮助。

转自https://www.cnblogs.com/codegb/p/18768600


该文章在 2025/3/14 9:53:11 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved