计算机字符编码详解——从理论到实践

时间:2023-01-10 22:08:40

前言


最近在看《深入理解计算机系统》,读到“字符编码”时不禁想起了初学时那段痛苦的岁月,同时又没找到一篇将理论和实践结合在一起的文章,为此决定自己写一份。希望能把我走过的弯路总结出来,能帮助一些还在路上的朋友。
关于计算机如何存储信息,请参考《深入理解计算机系统》的第 02 章内容,此文只讲解与字符编码有关的内容。
另外,关于常用编辑器对于字符编码的区别,请参考我的另一篇文档——《Win记事本、Sublime、Notepad++等编辑器对常见字符编码的处理和区别:GB2312、GBK、ANSI、Unicode、UTF-8》,此处也不再赘述。


字符的表示原理


简单说,计算机内所有信息都是使用0和1进行表示的对于一个短路来说,0代表关,1代表开。那把这些电路组合起来就可以有长串0和1组成的二进制数字,我们对这些数字进行编码和解码,我们就能用它来表示我们想要表示的东西了。比如:文字、图像、视频等等,就是一组0和1的二进制序列。
二进制数的每一个位表示一个计算机位(bit,简称位),8个位组成一个字节(byte)。那么一个字节可以表示256种含义(2*2*2*2*2*2*2*2=256)。

虽然机器是基于二进制的,但对人类来说,因为二进制数太长了,需要做精简。因此需要将其转换成十六进制(hexadecimal,简称hex)。转换方式很简单,使用“8421法”将四位二进制数转换成十六进制数的一位,比如:1010(binary)会转为A(hex)。在 C 语言中,十六进制数以”0x”或“0X”开头,A表示10,F表示16。

此后,00000000~11111111就可以用0x00~0xFF来表示了。


常见的字符编码

ANSI是美国国标的英文字符编码,占用一个字节,收录了127个字符。
原理很简单,用一个字节的不同的值表示一个字符、字母或数字。

文字开头有不可见的标志码,用于告知下文应该以什么方式解码。
文中所有字符编码都是这种方式。

英文字符编码占用一个字节就够了,但对于像中文、日文这种象形文字国家一个字节就肯定不够了。为此,中国的国标字符编码需要占用 2 个字节,中国标准局依次发布了GB2312、GBK、GB18030等。

虽然每个国家或地区的字符编码自行搞定了,但是多个国家间还是不能兼容。为此Unicode编码应运而生,它整合全世界的几乎所有语言文字。

下面我们依次讲解下ANSI、GB2312、GBK、GB18030、Unicode、UTF-8等编码。

ANSI编码

ANSI码占用1个字节,收录127个字符,取值范围0x00~0x7F

ANSI编码表:
计算机字符编码详解——从理论到实践

GB2312、GBK、GB18030

发布顺序依次为:GB2312、GBK、GB18030。都以ANSI格式存储

彼此区别:

  • GB2312是常用简体汉字;
  • GBK基于GB2312,包含更多生僻字,支持简繁双体
  • GB18030基于GBK,包含更多生僻字,支持少数民族文字,支持CJK(中日韩)统一字符。
  • GB2312和GBK都是双字节,GB18030分单、双、四字节三个部分。


比如:在GB2312上没有“喆”,在GBK上才被收录。

因这三种编码规则差不多,下面我们重点讲解下GB2312。


GB2312

考虑到以下因素:

  • ANSI范围是:0x00~0x7F。
  • 国际通用精确数字计算使用的BCD编码范围是:0x00~0x99。

为避免冲突,要预留一定空间,GB2312的范围:0xA0~0xFF

GB2312:

  • 又称GB0,国标局发布,1981年5月1日实施。
  • 收录6763个汉字:一级汉字3755个,常用字,排在前面;二级汉字3008个,生僻字,排在后面。
  • GB2312是一种区位码。分为94个区(01-94),每区94个字符(01-94)。
    • 01-09区为特殊符号;
    • 10-15区没有编码;
    • 16-55区为一级汉字,按拼音排序,共3755个;
    • 56-87区为二级汉字,按部首/笔画排序,共3008个;
    • 88-94区没有编码;
    • GB2312只是编码表,在计算机中通常都是用”EUC-CN“表示法,即在每个区位加上0xA0来表示。区和位分别占用一个字节。

EUC-CN表示法:

  • EUC-CN是GB2312最常用的表示方法。
  • GB2312字元使用两个字节来表示, 每个字节=0xA0+区位码。
  • “第一位字节”取值范围:0xB0~0xF7
  • “第二位字节”取值范围:0xA1~0xFE

举例来说,“啊”字是GB 2312之中的第一个汉字,它的区位码是1601
在EUC-CN之中,它把0xA0+16=0xB0;0xA0+1=0xA1;得出0xB0A1

GB2312的中文区位的首末两位是特意空出来的。原因是:

  • 首位(0x00):在ANSI中表示空,中国人更习惯从1开始计数,而不是从0开始计数。
  • 末位(0xFF):代表结束,因为有些程序员会将其定义为空,因此空出以避免冲突。

GB2312中文编码页的排列方式:
计算机字符编码详解——从理论到实践

GB2312的编码表请参考以下链接:GB2312汉字编码字符集对照表

《关于Windows记事本与Sublime Text对中文字符编码转换的问题》:

先对Windows记事本做个小实验:

  1. 在Windows记事本中,新建文件,输入“你好hello1234”,以ANSI格式保存后;
  2. 在Sublime Text中打开此文件,文件格式为GB2312,输入GB2312内不支持的汉字“喆”,提示保存失败,原因是’\u5586’是非法的多字节序列;
    ’gb2312’ codec can’t encode character ‘\u5586’ in position 11: illegal multibyte sequence
    第 11 个字符就是“喆”。
    注意:如果不删除这个字段,Sublime Text会以UTF-8格式重新保存这个文件。

  3. 在Windows记事本中,输入GB2312内不支持的汉字“喆”,按ANSI格式保存成功;

  4. 再次用Sublime Text打开此文件,文件格式已变为GBK。

综上,在Windows记事本中,GB2312和GBK是统一以ANSI方式管理的。我猜测,Windows记事本默认使用GB2312存储汉字,如遇到GB2312不能识别的生僻字才会使用GBK格式存储。

Sublime Text并不支持这种功能,如果你强行输入“喆”字以后,会有错误框弹出。然后,它会把这个文件转换为UTF-8格式并保存。
因为Sublime Text默认是支持UTF-8编码,不支持GB2312、GBK等中文编码,需要安装ConvertToUTF-8插件才能实现。显然,ConvertToUTF-8并没考虑这种情况。

在Sublime Text中以GB2312格式无法存储没被收录的字符:
计算机字符编码详解——从理论到实践

Unicode

Unicode简介:

  • 统一码、万国码、单一码,包括字符集、编码方案等。
  • 它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
  • 1990年开始研发,1994年正式公布。
  • 统一码为每个字符而非字形定义唯一的编码(即一个整数),字体视觉展示由其他软件来处理。由此,可解决汉字一字多形的认定争议(如“ɑ/a”、“户/户/戸”等)。

Unicode与UCS:
统一编码联盟都试图独立创建一套国际通用的字符编码,此后双方都意识到没必要创建两套编码,于是两者融合并承诺彼此兼容。从Unicode 2.0开始,Unicode采用了与ISO 10646-1相同的字库和字码。

通用字符集(Universal Character Set, UCS):由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所定义的标准字符集。UCS-2用两个字节编码,UCS-4用4个字节编码。

简单说,Unicode与UCS-2相同。

Unicode编码原理:

UCS-4有4个字节:

  • 第一个字节:表示组(group),最高位为0,则有128个。
  • 第二个字节:表示平面(plane),256个。
  • 第三个字节:表示行(row),256个。
  • 第四个字节:表示码位(cell),256个。


group 0的是最底层的平面0被称作BMP(Basic Multilingual Plane),即0x0000XXXX(大写X表示任何一个十六进制数字)。

平面 字符范围 备注
基本多语言平面 0~0xFFFF BMP, or Plane 0
增补多语言平面 0x10000~0x1FFFF SMP, or Plane 1
增补表意字符平面 0x20000~0x2FFFF SIP, or Plane 2
增补专用平面 SSP, or Plane 14

注意:

  • BMP:Basic Multilingual Plan
  • SMP:Supplementary Multilingual Plane
  • SIP:Supplementary Ideographic Plane
  • SSP:Supplementary Special-purpose Plane

当UCS-4的前两个字节为全零(BMP)时,那么去掉UCS-4的前两个零字节就得到了UCS-2。

在Unicode中,有几种方式唯一编码数字的转换格式:UTF-8、UTF-16、UTF-32。
UTF(Unicode Transformation Format),可以翻译成Unicode字符集转换格式,即怎样将Unicode定义的数字转换成程序数据。


例如,“汉字”对应的数字是0x6c49和0x5b57,而编码的程序数据是:

char      data_utf8[]={0xE6,0xB1,0x89,0xE5,0xAD,0x97};  //UTF-8编码
char16_t data_utf16[]={0x6C49,0x5B57}; //UTF-16编码
char32_t data_utf32[]={0x00006C49,0x00005B57}; //UTF-32编码

注: char16_t 和 char32_t 是 C++ 11 标准新增的关键字。如果你的编译器不支持 C++ 11 标准,请改用 unsigned short 和 unsigned long。

“汉字”的UTF-8编码需要6个字节。
“汉字”的UTF-16编码需要两个char16_t,大小是4个字节。
“汉字”的UTF-32编码需要两个char32_t,大小是8个字节。

UTF-8

Unicode编码(十六进制) UTF-8 字节流(二进制)
00000000 - 0000007F 0xxxxxxx
00000080 - 000007FF 110xxxxx 10xxxxxx
00000800 - 0000FFFF 1110xxxx 10xxxxxx 10xxxxxx
00010000 - 001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
00200000 - 03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
04000000 - 7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8的特点是对不同范围的字符使用不同长度的编码,兼容ASCII编码。

  • 对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。
  • UTF-8编码的最大长度是6个字节。
  • 从上表可以看出,6字节模板有31个x,即可以容纳31位二进制数字。Unicode的最大码位0x7FFFFFFF也只有31位。

例1:
“汉”的Unicode编码是0x6C49,在0x0800-0xFFFF之间,使用3字节模板:1110xxxx 10xxxxxx
10xxxxxx。 0x6C49的二进制表示为:0110 1100 0100 1001,
用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。


例2:Unicode编码0x20C30在0x010000-0x10FFFF之间,使用4字节模板:11110xxx 10xxxxxx
10xxxxxx 10xxxxxx。将0x20C30写成21位二进制数字(不足21位就在前面补0):0 0010 0000 1100
0011 0000,用这个比特流依次代替模板中的x,得到:11110000 10100000 10110000 10110000,即F0
A0 B0 B0。

UTF-16

根据字节序的不同,UTF-16可以被实现为UTF-16BEUTF-16LE(Windows系统默认)。

Win32的API提供两种调用方式:

  • 以W结尾的函数:使用UTF-16LE编码模式;
  • 以A结尾的函数:使用多字节编码模式(中文编码是以ANSI格式存储的),但是Windows会在底层将其转换为UTF-16LE编码来处理。

因此对于Windows编程来说,最好默认使用Unicode(UTF-16LE)模式开发,避免系统底层多余的字符转换。

Unicode字符编码范围 UTF-16 单个字符长度 备注
U+0000~U+FFFF 1个16位编码单元 16位无符号整数,固定宽度,BMP中的字符
U+10000~U+10FFFF 2个16位编码单元 称作代理对(surrogate pair),变宽

UTF-16是早期Unicode遗留下的历史产物,原本被设计成具有固定宽度的16位编码格式。为支持超过U+FFFF的增补字符,设立了代理机制。

编码转化规则

UTF-16编码以16位无符号整数为单位。我们把Unicode编码记作U。编码规则如下:

  • 如果U<0x10000,U的UTF-16编码就是U对应的16位无符号整数。
  • 如果U≥0x10000,
    • 我们先计算U’=U-0x10000,
    • 然后将U’写成二进制形式:yyyy yyyy yyxx xxxx xxxx,
    • U的UTF-16编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。

为什么U’可以被写成20个二进制位?
Unicode的最大码位是0x10FFFF,减去0x10000后,U’的最大值是0xFFFFF,2的5次幂是32,所以肯定可以用20个二进制位表示。

例如:Unicode编码0x20C30,减去0x10000后,得到0x10C30,写成二进制是:0001 0000 1100 0011
0000。用前10位依次替代模板中的y,用后10位依次替代模板中的x,就得到:1101100001000011
1101110000110000,即0xD843 0xDC30。

UTF-32

根据字节序的不同,UTF-32可以被实现为UTF-32LE或UTF-32BE。

UTF-32是一种最简单的Unicode编码格式。每个Unicode码点被直接表示为一个32位的编码单元。UTF-32是一种固定宽度的字符编码格式。
每个UTF-32编码单元的值与Unicode码点的值完全相同。

因为对于多数情况下,非常浪费空间,因此应用场景很少。


各编码间的转换


总结


参考文献

深入理解计算机系统——第02章——信息的表示和处理
GB2312的区位码是94的来历
GB2312汉字编码字符集对照表
“字节序”是个什么鬼?
字符编码笔记:ASCII、Unicode、UTF-8、UTF-16、UCS、BOM、Endian
Unicode汉字对应表
US-ASCII和ISO-8859-n 控制字符表
Unicode字符编码标准