一、原始的ASCII编码

以下是原始的ASCII编码对应关系图表

img

img

可以来一个小实验:打开记事本,编辑AB,另存为格式为ANSIASCII.txt文件

img

使用UE查看十六进制,就会发现的确是AB对应的十六进制数41 42,其中左侧的00000000h是这两个字符的相对偏移

img

二、ASCII的拓展:GB2312或GB2312-80

1个字节,最多也就能表示[0,255]总共256个字符,而像汉字,日文,韩文等等,这些在ASCII码表中都没有表示方法所以就必须要创建新的编码

于是GB2312和GB2312-80诞生了,思路就是:既然ASCII码的扩展表字符不常用,那就针对扩展的ASCII码,把两个扩展的ASCII码字节拼在一起,变成一个新的汉字。比如C4 E3就是BA C3就是

但是这种编码方式有个很严重的问题,每个国家都有每个国家的编码,比如发送一篇编码方式为GB2312,内容为你好,的邮件给新加坡,而新加坡用的是big5编码,也是用两个拓展ASCII的字节拼成一个新的字,但它用big5拼出来肯定不是你好二字,是乱码

img

三、Unicode编码

1、什么是Unicode?

这是一个编码方案,说白了就是一张包含全世界所有文字的一张编码表,只要是这个世界上存在的文字符号,统统给你一个唯一的编码。

Unicode编码范围是:0x000000——0x10FFFF,可以容纳100多万个符号!

2、Unicode的问题

Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储

假设是:1234,那么就是12 34这样存储,占两个字节;12345,那到底该怎么存储,01 23 45还是12 34 50又或者是其他方式呢?不得而知

3、如何存储Unicode:UTF-16/UTF-8

UTF-16/UTF-8是Unicode的实现方式

(1)UTF-16(Unicode的默认存储方式)

UTF-16编码以16位无符号整数单位,注意是16位为一个单位,不表示一个字符就只有16位。这个要看字符的Unicode编码处于什么范围而定,有可能是2个字节,也有可能是4个字节。

没有满两个字节的就补全到两个字节,满两个字节的就补全到四个字节,以此类推。

现在机器上的Unicode编码一般指的就是UTF-16。

img

做个小实验,文本文件Unicode.txt的内容为A中且用Unicode编码,用UE打开该文件,查看十六进制。计算机中默认是小端存储,首先前两2个字节0xFEFF先别管(后续会说),0x0041A0x4E2D,没问题

img

但这样就会有个问题,A0x41一个字节就可以存储,用两个字节存储0x0041造成了资源浪费,这在本地存储还没什么,顶多费点硬盘;主要是在网络传输中,本来4k能传完的东西要传8k,浪费了带宽,于是便诞生了UTF-8来解决这个问题,而UTF-8也正是在网络传输问题上火起来了

UTF-32即是以四个字节为单位,更暴力了

(2)UTF-8

UTF-8是一种变长的存储方案,顾名思义,1个字节能存储的就用1个字节,2个字节能存储的就用2个字节…….,优点就是节省空间缺点就是解析困难

UTF-8的编码规则很简单,只有二条:

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

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

分类 Unicode编码(16进制) UTF-8字节流(二进制) 特点
1 0000 0000 - 0000 007F 0xxxxxx 是咋样就咋样
2 0000 0080 - 0000 07FF 110xxxxx 10xxxxxx 一分为二
3 0000 0800 - 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx 一分为三
4 0001 0000 - 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 一分为四

举个例子,由上面的实验可知以下信息

A中-->Unicode编码:0x0041 0x4E2D

现在将A中进行UTF-8编码,结果如下图:前面的三个字节0xBFBBEF先不用管,后面是41 E4 B8 AD

img

为什么会这样呢?看下面的分析

首先是已知信息
A中-->UTF-16编码:0x0041 0x4E2D
A中-->UTF-8编码: 0x41 0xE4B8AD(UTF-8以大端存储)

我们从已知信息来分析为什么会产生这样的结果
[+] A的UTF-16编码的结果为0x0041,在上表中第一种情况的区间内,因此是咋样就咋样,UTF16的A转换为UTF-8的A保持不变
[+] 中的UTF-16编码的结果为0x4E2D,在上表中第三种情况的区间内,因此按照以下规则一分为三:
[---] 0x4E2D的二进制是0100 1110 0010 1101,将得到的二进制数据依次填充到1110xxxx 10xxxxxx 10xxxxxx中就是1110(0100) 10(111000) 10(101101),即E4 B8 AD,即0xE4B8AD
[---] 同样的,反向分析的话,0xE4B8AD二进制为1110(0100) 10(111000) 10(101101),以四位即十六进制提取出来即0100 1110 0010 1101,即4E 2D,即0x4E2D

因此可以得出一个一般性的结论:当网络传输文件的时候,数字字母符号比较多,就用UTF-8更节省资源,如果汉字比较多,用UTF-8节省不了多少资源,使用UTF-16即可

(3)BOM(Byte Order Mark)

如果接收到了一个文件,我们怎么知道是用UTF-8还是用UTF-16解析呢?这就有了字节序标记来解决这个问题

编码 数据流 大小端
UTF-8 EF BB BF 大端存储
UTF-16 LE(little endian) FF FE 小端存储
UTF-16 BE(big endian) FE FF 大端存储

为什么UTF-16需要区分大端小端,而UTF-8不用呢?

对UTF-16来说,因为是以两个字节为单位,两个字节正着放,和反着放,会对应两个不同字上面去。

img

对UTF-8来说,由于UTF-8的设计,一个字符对应的各个字节,是能够区分出哪个是数据高位、哪个数据低位的,所以不需要。上表中,第一个字节就属于是数据高位。

不管是内存、还是文件,程序总是从低地址读到高地址,而由于第一个字节的开头可以指示出这个字符还剩余几个字节需要读取,所以UTF-8必须把可以指示剩余字节数的那个字节放到低地址,其余字节依次放到高地址。

四、大小端存储方式

计算机默认是小端存储

小端存储:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。

大端存储:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放。(大端模式是我们直观上的认为的模式)

如果将一个32位的整数0x12345678存放到一个整型变量(int)中,这个整型变量采用大端或者小端模式在内存中的存储由下表所示。

为简单起见,本文使用OP0表示一个32位数据的最高字节MSB(Most Significant Byte),使用OP3表示一个32位数据最低字节LSB(Least Significant Byte)。

img

小端:较高的有效字节存放在较高的存储器地址,较低的有效字节存放在较低的存储器地址。
大端:较高的有效字节存放在较低的存储器地址,较低的有效字节存放在较高的存储器地址。

测试代码

#include <stdio.h>

void main() {
int i = 0x12345678;
char* pc = (char*)&i;
if (*pc == 0x12) {
printf("Big Endian\n");
}
else if (*pc == 0x78) {
printf("Little Endian\n");
}
}

img