谈谈字符编码

encoding

1. 写在前面的话

很多初级甚至有丰富开发经验的开发者都可能会遇到乱码的问题,对深入理解字符编码的人来说,问题很容易解决。但如果你对字符编码一知半解,这些问题可能会让你抓狂。对一名开发者来讲,不管你使用什么语言,在什么平台上开发,字符编码都是需要掌握的基本技能之一。

我们都知道,计算机只能处理0和1,为了让计算机能够处理我们的信息,就需要对信息进行编码。信息编码是一个很大的主题,本文只涉及计算机字符的编码,包括ASCII、Unicode、UTF-8、UTF-16、UCS-2。

2. ASCII

对于ASCII大家都比较熟悉,由于当时硬件和计算资源都很匮乏,ASCII只使用了前7位,可以表示128个字符,如048(二进制0011 0000),a97(二进制0110 0001)。

对于英语来说,128个符号已经够用了,但对其它语言来说,128个符号是不够的,当计算机传入欧洲后,有些欧洲国家利用闲置的最高位编入新的符号,这样就可以最多表示256个符号。IBM当时针对不同语言,在(128-255)区间内OEM了不同的编码方式,这也造成了同一个编码可能表示不同的符号。

对于亚洲国家就更惨了,只汉字就多达10万左右,一个字节根本表示不了这么多符号,于是就有了两个字符表示一个符号,这样理论上最多可表示256 x 256 = 65535个字符,这对表示常用的汉字就够用了。

鉴于以上各种编码方式的混乱,于是出现了Unicode

3. Unicode

Unicode编码就如其名字一样,旨在为全世界所有的符号进行编码,且每个符号的编码都不一样。其编码空间能容纳上百万个字符。如用U+0030表示0,用U+4E2D表示。需要注意的是:Unicode只是一个字符集,它定义了每个符号的二进制代码,它并没有规定这些二进制代码在计算机中如何存储。更多关于Unicode的知识请参考unicode.org维基百科

仍然以为例,其Unicode的十六进制表示为4E2D,对应的二进制为100111000101101,足足有15位,所以至少需要2个字符来存储。对于更大编码的字符,可能需要3个甚至4个字符来存储。这里有两个问题必须考虑:

1. 如果用足够容纳所有字符的字节,比如4个字节来存储,这样对英语语种来说,以前每个字母只需要1个字节,现在却需要4个字节来存储,而且前面三个字节全是0,这对计算机存储资源是极大的浪费!

2. 如果采用变长的字节来存储,比如对ASCII字符仍然采用1个字符,对汉字采用2个或3个字节,那么计算机如果区分1个字节表示一个字符或2个字节甚至3个字节表示一个字符呢?

于是各种编码标准应用而生,其中应用比较广泛的是UTF-8。

4. UTF-8

什么是UTF-8?它跟Unicode是什么关系?
UTF是 Unicode/UCS Transformation Format 的首字母缩写,即把Unicode字符转换为某种格式之意。UTF-8只是Unicode字符集的一种编码实现方式之一。其它实现方式还有UTF-16和UTF-32等。UTF-8最大的特点是它是一种变长编码方式,它可以用1~4个字节表示一个字符,其编码规则也比较简单,简述为以下两条规则:

1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的Unicode 码。因此对于英语字母,UTF-8编码和ASCII码是相同的。

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

下表总结UTF-8的编码规则,其中x表可编码的位:

Unicode符号范围(十六进制) UTF-8编码(二进制)
0000 0000 - 0000 007F 0xxxxxxx
0000 0080 - 0000 07FF 110xxxxx 10xxxxxx
0000 0800 - 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 - 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

我们仍然以以为例,演示如何进行UTF-8编码:
的Unicode为4E2D(100111000101101),其Unicode在第三行0000 0800 - 0000 FFFF之间,于是其对应的二进制格式应该为1110xxxx 10xxxxxx 10xxxxxx。接下来,只需要把的二进制位从最低位开始,依次填入x中即可,最终得到的UTF-8编码为11100100 10111000 10101101,转化为十六进制为E4B8AD

5. UCS2 & UTF-16

Unicode的编码空间从U+0000U+10FFFF,共有1,112,064个码位(code point)可用来映射字符。Unicode的编码空间可以划分为17个平面(plane),每个平面包含65,536(2的8次方)个码位。17个平面的码位可表示为从U+xx0000U+xxFFFF,其中xx表示十六进制值从0x000x10,共计17个平面。第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0)。其他平面称为辅助平面(Supplementary Planes)。基本多语言平面内,从U+D800U+DFFF之间的码位区块是永久保留不映射到Unicode字符。UTF-16就利用保留下来的0xD800 - 0xDFFF区段的码位来对辅助平面的字符的码位进行编码。

UCS即通用字符集(Universal Character Set, UCS),在国际标准ISO10646中定义,和Unicode一样,它只定义了每个符号的二进制代码,位并没有规定这些二进制代码在计算机中如何存储。UCS-2表示用两个字节来存储。

UCS-2可以看成是UTF-16的子集。在没有辅助平面字符前,UCS-2和UTF-16所指的是同一的意思。但当引入辅助平面字符后,就称为UTF-16了。对于UCS-2编码,其实是暗指它不能支持在UTF-16中超过2字节的字集。对于小于0x10000的UCS码,UTF-16编码就等于UCS码。

具体关于UTF-16的编码解码规则请参考RFC文档

5. Windows对字符的处理

5.1 记事本程序

我们平时用Window记事本程序notepad.exe保存文件时可能没有注意到,当我们选择另存为(Save As...)时,可以指定编码方式:

Save As...

可以看到notepad.exe程序为我们提供了4种类型的编码方式:ANSI、Unicode、Unicode big endian和UTF-8。如果我们新建4个文本文件,按照4种编码方式分另在每个文本中都保存一个字,再用WinHex软件打开看看其二进制内容如下:

编码方式 十六进制
ANSI D6 D0
Unicode FF FE 2D 4E
Unicode big endian FE FF 4E 2D
UTF-8 EF BB BF E4 B8 AD
  • ANSI是默认的编码方式。这个跟具体的操作系统语言选择有关,对于英文文件是ASCII编码,对于简体中文文件是GB2312编码。
  • Unicode编码是指使用的UCS-2编码方式,关于UCS-2上面已经讲过,即直接用两个字节存入字符的Unicode码,这个选项用的 little endian 格式。
  • Unicode big endian编码与上一个选项相对应。
  • UTF-8编码,上面已经介绍过了。

细心的读者可能已经观察到,对于Unicode和Unicode big endian有两个地方不同:

  1. Unicode和Unicode big endian每两个字节的存储顺序相反,如FF FE 2D 4EFE FF 4E 2D
  2. 文件开头都加了一个FEFF(Unicode顺序相反为FFFE

对于第一个问题,涉及到大小端序,p它是CPU寻址的不同方式,简单地说就是:
大端序:最高位字节存储在最低位内存地址处,次高位字节存储在次低位内存地址中,以此类推。
小端序:最低位字节存储在最低的内存地址处,次低位字节存储在次低位内存地址处,以此类推。

以字节一个32位(4字节)的整数0x0A0B0C0D为例,以大端序方式存储为:


以小端序方式存储为:




对于第二个问题是Unicode规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做零宽度非换行空格(zero width no-break space),也称为BOM(byte-order mark),用FEFF表示,这正好是两个字节,按照大小端序的定义,文件开头存储的是FEFF表示大端序,存储的是FFFE表示小端序。

而对于UTF-8,其字节顺序是不变的,不存在大小端序的问题。如果以UTF-8编码的文件开头有BOM,按照UTF-8编码规则,FEFF被编码为EFBBBF,这个文件被称为使用UTF-8-BOM编码;如果文件开头不包含BOM,我们通常说这个文件为使用UTF-8编码。我们使用notepad++等文本编辑器时,在编码中可以看到:

notepad++ encoding

5.2 VC++对字符的处理

Windows操作系统内核中的字符表示为UTF-16小端序,可以正确处理、显示以4字节存储的字符。但是Windows API实际上仅能正确处理UCS-2字符,即仅以2字节存储的,码位小于U+FFFF的Unicode字符。其根源是Microsoft C++语言把wchar_t数据类型定义为16比特的unsigned short,这就与一个wchar_t型变量对应一个宽字符,可以存储一个Unicode字符的规定相矛盾。

除了上节说的用记事本进行字符的编码转化外,Windows提供了MultiByteToWideCharWideCharToMultiByte两个API用于多字节与宽字节的两互转化。关于这两个API的具体使用方式,可以参考MSDN。对于VC++,这里简单说一下平时使用比较多的两种转化,仍然以为例,上面已经提到,在简体中文系统里面,ANSI实际上是按GB2312进行编码,其编码为D6D0,UTF-8编码为E4B8AD,转化成Unicode应该为4E2D

  1. ANSI转Unicode


    ansi 转 unicode
  2. UTF-8转Unicode


    utf-8 转 unicode

注:需要说明的上述代码跟平台相关,最后对结果之所以采用这种断言方式,_ASSERT(unicode[0] == 0x2D && unicode[1] == 0x4E);是因为Windows操作系统默认采用小端序,如果你忘记了什么是小端序,请回到上节温习一下。

对于Unicode转ANSI和Unicode转UTF-8只是上面的逆过程,使用 WideCharToMultiByte,修改相应参数即可,在此不在累赘。

6. 总结

  • Unicode只是一个字符集,它定义了每个符号的二进制代码,它并没有规定这些二进制代码在计算机中如何存储;
  • UTF-8只是Unicode字符集的一种编码实现方式之一。其它实现方式还有UTF-16和UTF-32等;
  • 在没有辅助平面字符前,UCS-2和UTF-16所指的是同一的意思。但当引入辅助平面字符后,就称为UTF-16了。对于UCS-2编码,其实是暗指它不能支持在UTF-16中超过2字节的字集。对于小于0x10000的UCS码,UTF-16编码就等于UCS码;
  • 大端序是指最高位字节存储在最低位内存地址处,次高位字节存储在次低位内存地址中,以此类推;小端序是指最低位字节存储在最低的内存地址处,次低位字节存储在次低位内存地址处,以此类推;

7. 更多参考

[1]. https://www.ietf.org/rfc/rfc3629.txt
[2]. https://www.ietf.org/rfc/rfc2781.txt
[3]. http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
[4]. https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/
[5]. https://msdn.microsoft.com/en-us/library/windows/desktop/dd319072(v=vs.85).aspx
[6]. https://msdn.microsoft.com/en-us/library/windows/desktop/dd317756(v=vs.85).aspx
[7]. https://en.wikipedia.org/wiki/Endianness

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,018评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,046评论 2 372
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,215评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,303评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,181评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,171评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,577评论 3 384
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,260评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,546评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,630评论 2 311
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,390评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,260评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,633评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,920评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,210评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,569评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,767评论 2 335

推荐阅读更多精彩内容

  • 字符是用户可以读写的最小单位。计算机所能支持的字符组成的集合,就叫做字符集。字符集通常以二维表的形式存在。二维表的...
    刘惜有阅读 8,038评论 2 14
  • 一直觉得:一个合格的程序员必须要了解清楚编码。 程序员的工作中很频繁的一个场景是:啊啊,又(中文)乱码了,怎么弄,...
    六层楼那么高阅读 852评论 0 1
  • 现今的西流湖显得落魄,让人心疼。过去没有见过以前西流湖胜景的人。不会想到这就是郑州从前的西湖。但愿郑州西流湖公园早...
    emmmmmm哦阅读 91评论 0 0
  • 昨天和朋友打电话,聊到最近在看什么书,我说在看穷查理宝典,他表示不知道。我说,那你知道巴菲特吧,他是巴菲特公司伯克...
    Cody小安阅读 434评论 0 0
  • 1:学习英语有很多种方法,但是兴趣最重要2:qualified 3:Inmypersonal,noonecanev...
    应数二班王璐阅读 435评论 0 0