在计算机中,所有数据都是以二进制数的形式存储的,字符char也不例外。为了表示字符,我们需要建立一套“字符集”,规定每个字符和二进制数之间的一一对应关系。有了字符集之后,计算机就可以通过查表完成二进制数到字符的转换。
「ASCII码」是最早出现的字符集,全称为“美国标准信息交换代码”。它使用7位二进制数(即一个字节的低7位)表示一个字符,最多能够表示128个不同的字符。如图3-6所示,ASCII码包括英文字母的大小写、数字0~9、一些标点符号,以及一些控制字符(如换行符和制表符)。
图3-6ASCII码
然而,ASCII码仅能够表示英文。随着计算机的全球化,诞生了一种能够表示更多语言的字符集「EASCII」。它在ASCII的7位基础上扩展到8位,能够表示256个不同的字符。
在世界范围内,陆续出现了一批适用于不同地区的EASCII字符集。这些字符集的前128个字符统一为ASCII码,后128个字符定义不同,以适应不同语言的需求。
后来人们发现,EASCII码仍然无法满足许多语言的字符数量要求。比如汉字大约有近十万个,光日常使用的就有几千个。中国国家标准总局于1980年发布了「GB2312」字符集,其收录了6763个汉字,基本满足了汉字的计算机处理需要。
然而,GB2312无法处理部分的罕见字和繁体字。「GBK」字符集是在GB2312的基础上扩展得到的,它共收录了21886个汉字。在GBK的编码方案中,ASCII字符使用一个字节表示,汉字使用两个字节表示。
随着计算机的蓬勃发展,字符集与编码标准百花齐放,而这带来了许多问题。一方面,这些字符集一般只定义了特定语言的字符,无法在多语言环境下正常工作。另一方面,同一种语言也存在多种字符集标准,如果两台电脑安装的是不同的编码标准,则在信息传递时就会出现乱码。
那个时代的研究人员就在想:如果推出一个足够完整的字符集,将世界范围内的所有语言和符号都收录其中,不就可以解决跨语言环境和乱码问题了吗?在这种想法的驱动下,一个大而全的字符集Unicode应运而生。
「Unicode」的全称为“统一字符编码”,理论上能容纳一百多万个字符。它致力于将全球范围内的字符纳入到统一的字符集之中,提供一种通用的字符集来处理和显示各种语言文字,减少因为编码标准不同而产生的乱码问题。
自1991年发布以来,Unicode不断扩充新的语言与字符。截止2022年9月,Unicode已经包含149186个字符,包括各种语言的字符、符号、甚至是表情符号等。在庞大的Unicode字符集中,常用的字符占用2字节,有些生僻的字符占3字节甚至4字节。
Unicode是一种字符集标准,本质上是给每个字符分配一个编号(称为“码点”),但它并没有规定在计算机中如何存储这些字符码点。我们不禁会问:当多种长度的Unicode码点同时出现在同一个文本中时,系统如何解析字符?例如给定一个长度为2字节的编码,系统如何确认它是一个2字节的字符还是两个1字节的字符?
对于以上问题,一种直接的解决方案是将所有字符存储为等长的编码。如图3-7所示,“Hello”中的每个字符占用1字节,“算法”中的每个字符占用2字节。我们可以通过高位填0,将“Hello算法”中的所有字符都编码为2字节长度。这样系统就可以每隔2字节解析一个字符,恢复出这个短语的内容了。
图3-7Unicode编码示例
然而ASCII码已经向我们证明,编码英文只需要1字节。若采用上述方案,英文文本占用空间的大小将会是ASCII编码下大小的两倍,非常浪费内存空间。因此,我们需要一种更加高效的Unicode编码方法。
目前,UTF-8已成为国际上使用最广泛的Unicode编码方法。它是一种可变长的编码,使用1到4个字节来表示一个字符,根据字符的复杂性而变。ASCII字符只需要1个字节,拉丁字母和希腊字母需要2个字节,常用的中文字符需要3个字节,其他的一些生僻字符需要4个字节。
UTF-8的编码规则并不复杂,分为以下两种情况。
图3-8展示了“Hello算法”对应的UTF-8编码。观察发现,由于最高n位都被设置为1,因此系统可以通过读取最高位1的个数来解析出字符的长度为n。
但为什么要将其余所有字节的高2位都设置为10呢?实际上,这个10能够起到校验符的作用。假设系统从一个错误的字节开始解析文本,字节头部的10能够帮助系统快速的判断出异常。
之所以将10当作校验符,是因为在UTF-8编码规则下,不可能有字符的最高两位是10。这个结论可以用反证法来证明:假设一个字符的最高两位是10,说明该字符的长度为1,对应ASCII码。而ASCII码的最高位应该是0,与假设矛盾。
图3-8UTF-8编码示例
除了UTF-8之外,常见的编码方式还包括以下两种。
从存储空间的角度看,使用UTF-8表示英文字符非常高效,因为它仅需1个字节;使用UTF-16编码某些非英文字符(例如中文)会更加高效,因为它只需要2个字节,而UTF-8可能需要3个字节。
从兼容性的角度看,UTF-8的通用性最佳,许多工具和库都优先支持UTF-8。
对于以往的大多数编程语言,程序运行中的字符串都采用UTF-16或UTF-32这类等长的编码。在等长编码下,我们可以将字符串看作数组来处理,这种做法具有以下优点。
实际上,编程语言的字符编码方案设计是一个很有趣的话题,其涉及到许多因素。
由于以上编程语言对字符数量的低估,它们不得不采取“代理对”的方式来表示超过16位长度的Unicode字符。这是一个不得已为之的无奈之举。一方面,包含代理对的字符串中,一个字符可能占用2字节或4字节,从而丧失了等长编码的优势。另一方面,处理代理对需要增加额外代码,这增加了编程的复杂性和Debug难度。
出于以上原因,部分编程语言提出了一些不同的编码方案。
需要注意的是,以上讨论的都是字符串在编程语言中的存储方式,这和字符串如何在文件中存储或在网络中传输是两个不同的问题。在文件存储或网络传输中,我们通常会将字符串编码为UTF-8格式,以达到最优的兼容性和空间效率。