前言
我们在开发中是不是经常会遇到这样的问题,比如你在VS2019中创建了一个工程,里面有C语言程序和中文注释,有一天,根据工作需要,你要把其中的一部分C文件和H文件移植到Keil工程中,当你通过复制黏贴把相应文件移植到Keil工程中,并使用MDK打开时,却发现,你移植的文件C语言程序是正常显示的,但是中文却成了一堆乱码,并且一编译各种莫名其妙的报错。这其实就有可能是你的VS2019和Keil使用了不同的编码方式,因为大部分编码兼容ASCII编码,而C语言程序是英文字符,采用了ASCII编码,所以正常显示,而中文编码就不同了,比如内存中同样的0xB0A1,使用不同编码标准去对0xB0A1解码,得到的可能就是不同的汉字。
在计算机世界中,只有0、1两种数字,不论是英文、中文还是数字,在计算机中都是以01的形式存储的。因此,要想把文字存储到计算机上,就要规定特定的01序列来表示文字。编码就是规定特定的01序列来表示文字的过程,编码表示了字符在计算机中的存储形式。
一、什么是编码,数据类型和编码有什么关系?
我们在计算机中经常见到的文字、数字、英文字母、图片、视频、音频等,这些信息在计算机中都是以二进制的形式存储的,因为内存条是电子元器件组成的,它们只有高电平低电平两种状态,即0和1两个值。实际上,我们所说的十进制、八进制等进制以及char、int、float等数据类型这些概念都是对于程序员而言的,比如十进制、十六进制只是一个数字对我们的表现形式不同,逢十进一或逢十六进一的区别;而数据类型,int、char、unsigned int等等,这些数据类型是对内存的解释不同,数据类型说明了这段内存所能表示的数据范围不同,比如char占一个字节,表示的数据范围是0~255,int是4字节,unsigned int表示无符号4字节数据。有时候在程序中我们会对变量进行类型转换,比如十进制转十六进制,又或者是char型转int型,这些转型都是对内存的解释(主要是内存的大小,数据的范围),比如char b,那么b占一个字节,我们让b=1,然后转型(int)b,其实b还是1,只不过它现在被解释为占据4个字节的内存。总之,上面这些情况,不管如何转换,同一个数据不会因为类型转换而改变内存中的实际数据,b在char类型时是00000001,转为int型后成了00000000000000000000000000000001,它还是那个1,不管是十六进制0x01还是十进制1,它在内存中都是上面的二进制。这是因为数据类型只是对内存的解释,而真正决定它们在计算机中的存储形式(是0001序列还是1110序列)的是编码,编码是指一个数据在计算机中的01序列是如何存储的。
数据类型是固定大小内存块的别名,它说明了这块内存所能表示的数据大小范围;
字符集(character set)定义了文字和二进制的对应关系,并给每个文字分配一个一对一的唯一编号;
字符编码(character Encoding)规定了文字的编号是怎么在计算机中存储的。
二、英文字符编码
ASCII编码
用8位二进制进行编码,用于表示控制字符、英文字符、数字字符。因为使用8位二进制编码,所以ASCII编码只能表示256个字符,编号范围为0~255。常用的ASCII码如下:
表示字符 | 十六进制形式 | 十进制形式 |
0~9 | 0x30~0x39 | 48~57 |
A~Z | 0x41~0x5A | 65~90 |
a~z | 0x61~0x7A | 97~122 |
不管是ASCII码的十六进制形式还是ASCII码的十进制形式,它都是一种解释性的概念,对内存数据的一种解释形式,用于表达给程序员看的概念,它们在计算机中的存储都是同样的二进制数,不会因为进制改变而改变。这种使用8位二进制来表示或存储字符的过程就叫做编码(一串二进制01和一个字符一一对应的过程)。这些用ASCII码表示的字符的集合叫做ASCII字符集。
在英文世界中,使用26个字母就可以拼写出全部的英文单词,每个字字母就是一个字符,所以,用8位的ASCII码就可以对整个英文世界进行编码。
三、中文编码
英文编码可以用字母编码来代替,这是因为所有英文单词都可以拆分成26个英文字母的组合。而中文就不一样了,中文一个字就是一个整体,只能按照一个字来编码,中文汉字成千上万,如果仅用8位ASCII码来编码,那么是明显不够的,ASCII码顶多表示256个汉字,所以就有了下面这些中文编码方式。
1. GB2312标准
GB2312总共覆盖了6763个常用汉字,GB2312标准把ASCII码表127号之后的扩展字符集去掉,并规定,小于127(0x7F)的编码按照ASCII标准进行解码,当出现连续两个大于127(0x7F)的编码时,这两个连续的大于0x7F的编码表示一个汉字,第一二个字节都是用0xA1~0xFE进行编码。其中,ASCII码中原有的数字字符、英文字符、标点等称为半角字符,大于0x7F的相应字符编码称为全角字符。
GB2312解码规则:当使用GB2312编码标准时,给定一串字符编码,按照字节进行检测,首先检测每个字节的大小,如果字节值小于0x7F,就用ASCII标准解码,如果连续两个字节的值都大于0x7F,就把这两个字节视为一个整体,使用GB2312标准解码。
举例:
0x61 | 0xB0 | 0xA1 | 0x61 |
从第一个字节开始检测,0x61小于0x7F,用ASCII标准解码,它表示英文字符“a”,第二个字节0xB0大于0x7F,第三个字节0xA1大于0x7F,连续两个字节大于0x7F,把它们连为一体使用GB2312解码为中文字符“啊”,第四个字节0x61小于0x7F,用ASCII标准解码,它表示英文字符“a”。综上,可解码如下
0x61 | 0xB0 | 0xA1 | 0x61 |
a | 啊 | a |
2. GBK编码
GBK编码在GB2312的基础上又增加了14240个汉字、生僻字和符号。按照GB2312的编码方式,两个字节已经不够用了,这时,GBK编码制定了新的标准:只要出现一个大于0x7F的字节,那么这个字节和它后面一个字节共两个字节就表示一个汉字(GB2312规定两个字节都大于0x7F才表示一个汉字),这样做的好处就是,GBK编码兼容了ASCII编码和GB2312编码。
GBK解码规则:当使用GB2312编码标准时,给定一串字符编码,按照字节进行检测,首先检测每个字节的大小,如果字节值小于0x7F,就用ASCII标准解码,如果遇到一个大于0x7F的字节,就把该字节和它后面一个字节连在一起用GBK标准进行解码,然后从第三个字节开始继续遍历检测。
举例:
0x61 | 0xB0 | 0x56 | 0x62 |
从第一个字节开始检测,0x61小于0x7F,用ASCII标准解码,它表示英文字符“a”,第二个字节0xB0大于0x7F,那么就把0xB0、0x56连在一起用GBK标准解码,然后从0x62开始继续检测,解码后如下
0x61 | 0xB0 | 0x56 | 0x62 |
a | 癡(chi) | b |
3. 其他中文编码
GB18030编码:使用4字节编码,兼容ASCII、GB2312、GBK。包括2000年编制的GB18030-2000,2005年编制的GB18030-2005。
Big5编码:多用于台湾香港等地,主要是收录了繁体字。在包含汉字数量上来说,Big5是GBK的子集,但是二者的编码方式是不同的,比如同样“啊”,GBK编码为0xB0A1,Big5编码为0xB0DA。
四、Unicode字符集
每个国家和地区都有一套自己的文字,不同的文字系统就要使用不同的编码标准,这就出现这样一个问题,同一个二进制编码在不同的编码标准中可能代表了不同的字符,比如0xB0A1,在GBK编码标准中为“啊”,而在Big5编码标准中就不是这个字了。这样,各个编码标准之间的不兼容就导致使用起来非常不方便。国际标准化组织ISO,将全球所有的语言所使用的字母、符号、文字进行统一编号,每个字符指定唯一一个标号与之对应(ASCII码编号不变),字符的编号从0x000000~0x10FFFF,该编号集称为Universal Multiple-Octet coded Character Set,简称UCS,一般也叫做Unicode。Unicode字符集仅仅是对所有字符进行了编号,并没有指定这些编号的编码规则,所以,后来才出现了各种Unicode的编码规则Unicode Transformation Format,典型的Unicode编码规则如UTF-8,UTF-16,UTF-32等。
1. UTF-32
Unicode Transformation Format 32,用32位(4字节)对Unicode字符集进行编码。编码时,Unicode字符集中的每一个字符都用4字节表示,直接把字符对应的Unicode编号转换为二进制数进行存储。而正因为UTF-32用4字节为每个字符编码,所以,UTF-32不兼容ASCII编码,使用ASCII编码标准写的程序,通过UTF-32编码方式打开会显示乱码。
解码时,直接按四个字节检测,编码与Unicode标号一一对应。
字符:A | ASCII编码:0x41 | Unicode编号:0x0000 0041 | UTF-32编码:0x0000 0041 |
字符:啊 | GBK编码:0xB0A1 | Unicode编号:0x0000 554A | UTF-32编码:0x0000 554A |
2. UTF-16
Unicode Transformation Format 16,用16位(2字节)或32位(4字节)对Unicode字符集进行编码。对Unicode字符编号在0~65535的字符使用2字节编码,将每个字符的编号直接转换为2字节的二进制数0x0000~0xFFFF。而Unicode字符集在0xD800~0xDBFF区间内的编号不表示任何字符,UTF-16用这段编号与Unicode字符集中大于0xFFFF的字符编号进行映射,得到扩展的4字节编码。UTF-16也不兼容ASCII编码。
UTF-16解码时,按两个字节去检测,如果这两个字节都不在0xD800~0xDFFF之间,就说明是双字节编码的字符,使用双字节解码;如果这两个字节在0xD800~0xDFFF之间,说明是4字节编码的字符,以4字节解码。
2. UTF-8
Unicode Transformation Format 8,用1,2,3,4个字节对Unicode字符集进行编码,每个字符根据自己的编号范围进行相应编码。它的编码规则是这样的:
对于UTF-8单字节的编码,该字节最高位设为0,剩余位填入字符的Unicode编号,对于Unicode编号在0x00000000~0x0000007F的字符,UTF-8编码只要一个字节,兼容ASCII编码。对于N字节的编码,第一字节最高位开始,前N位置为1,第N+1位设0,剩余字节最高位设为10,这N个字节的其余空位填充该字符的Unicode编号,高位补0。具体可见下表:
Unicode编号 | UTF-8编码 | |||
第1字节 | 第2字节 | 第3字节 | 第4字节 | |
0x00000000~0x0000007F | 0xxx xxxx | |||
0x00000080~0x000007FF | 110x xxxx | 10xx xxxx | ||
0x00000800~0x0000FFF | 1110xxxx | 10xx xxxx | 10xx xxxx | |
0x00010000~0x0010FFFF | 1111 0xxx | 10xx xxxx | 10xx xxxx | 10xx xxxx |
解码时,看第一个字节
0开头:单字节解码;
110开头:双字节解码;
1110开头:三字节解码;
11110开头:四字节解码;
UTF-8编码的时候,汉字一般是占三个字节的。
3. BOM
Byte Order Mark,我们在Notepad++中的Encoding选项中可以看到诸如Encoding in UTF-8,以及Encoding in UTF-8-BOM这样的选项,带不带标签不会影响对字符的编码解码,假如把不带标签的UTF-8编码转换为带标签的UTF-8-BOM编码,程序和中文注释都不会出现乱码,但是这两种是有区别的。比如我们常用的emWin,在emWin中文支持中,它只支持不带标签的UTF-8编码,如果使用带标签的UTF-8-BOM,虽然不会报错,但是在控件中,无法显示这些中文。
五、ANSI编码
ANSI编码是Windows中的一种称呼,像GBK、GB2312都是ANSI编码,在不同语言的操作系统中,ANSI表示的编码是不同的,比如中文、泰文、法文都有各自的编码方式,这些编码方式对ASCII编码的扩展就是ANSI。