最近在学习串这个数据结构,顺便把编码问题解决了,以后别再绕自己了。

参考链接

我查找了许多材料,发现Li Yucang 大佬讲述的非常详细 ,可惜我没有早早找到,本分的utf16的原理就摘抄大佬的讲解

字符集和编码

在计算机世界,一切都是二进制数字,所以文字到数字需要编码,从二进制到数字需要解码.,这里我们就需要 字符到数字的映射关系.

  • 字符: 就是各种文字和符号的总成,比如各国的文字,数字,标点符号,图像符号等.

  • 字符集: 字符集就是多个字符的集合.

    当然更合适理解, 一个规则集合的名字,包含的规则就是字符编码. 比如 ASCII 字符集,GB2312字符集,Unicode 字符集.

  • 字符编码,其实就是一种标准,规定了字符到数字的映射关系. 比如 UTF-8 ASCII 等

重点: 字符集只是一个一类符号的集合,但是没有规定使用哪种编码方式实现. 一种字符集可以由不同的子符编码实现,但是也有 字符集的名字就是 字符编码的名字,本来这个概念就很混, 不必太纠结.

常见字符集(编码)

  1. ASCII , 一个字符占一个字节,最高位 为0

  2. GB2312 ,最早的一版 中文编码

    GB2312是对 ASCII的扩展, 一个汉字用两个字节存储, 那这2bytes最高位不可以为0了(否则和ASCII会有冲突,也就是使用1来标记中文字符),收录了常用的简体字符

    值得注意的是:这个表中也有一些字符在ascii中也有 ,但是 对于的编码不一样(也就是半角和全角之分),我们在编程序的时候都使用的半角.

  3. GBK ,是 GB2312的扩充,由于GB2312只有6763个汉字,我汉语博大精深,只有6763个字怎么够?

    不和GB2312、ASCII冲突(即兼容GB2312和ASCII)并且使用 2个字节的前提下,由多收录了 许多汉字,总计20902个汉字,囊括了繁体字,当然这个湾湾的big5是冲突的.

  4. GB18030,直接使用4个字节保存汉字.

  5. Unicode 这个有点复杂等会介绍

  6. ANSI表示英文字符时用一个字节,表示非英文用两个字节,在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。

我们在txt文件里存储 hello 越行勤 保存分别使用 ANSI和 UTF-8

ANSI:

image-20210403180439189

UTF-8

image-20210403180524862

我们先不管 蓝色部分,我们可以清晰的看出,虽然存储的字符是一样的,但是编码却不同. 但是 对于英文来说 , hello 都是68 65 6C 6C 6F ,中文部分就不一样了.不管采用哪种编码,英文在底层都是使用 ascii编码. ASCII 编码 单字节编码, 适用英文编码.ASCII才使用一个字节 ,最多表示 2^8个文字,所以表示汉字是远远不够的.

Unicode

不同国家的文字不同,我国扩展了ascii码 规定了 GBK,其他国家比如日本指定了 JIS.导致一样的编码可以对于多个字符串,这两国交换信息还不得麻烦死.这让我咋看 学习资料呢!

Unicode编码

所以咱们需要一种容纳世界上所有文字和符号的编码方案.Unicode字符集容纳全世界所有字符,并且还能兼容 ascii.

Unicode编码给世界上所以的字符都规定了编码,给每一个字符都规定了一个编号,这个编号就叫这个字符的码点

目前来说,码点的范围是从 U+010000 一直到 U+10FFFF(U+表示紧跟在后面的十六进制数是 Unicode 的码点),目前整个 Unicode 字符集的大小是 2^21,最大一共三个字节。

当然这些编码还没用完呢! Unicode采用了分区定义,每个区可以存放 65536 个,对应十六进制范围 0000-FFFF , 那么一共划分了多少个分区呢? (10)H个换成十进制也就是 17, 叫做17个平面

目前第一个平面,也就是 00000-0FFFF 存储者最常用的65536 个字符,叫做基本平面(BMP),这是 Unicode 最先定义和公布的一个平面. 也就是使用两个字节存储了大多数字符,其他字符存储在 辅助平面(缩写 SMP) ,也就是剩下的空间。

基本汉字的码点范围是 4E00-9FA5,也就是 一个中文字符最少需要 2个字节。

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

存储方案如下:

方案1 UTF-32

采用 定长的编码方式

很简单,采用定长的编码方式,一个字节不够,那么就用多个字节.但是这样有一个坏处,太浪费内存了,如果使用 3个字节单单去表示中文和英文,那么其他的bit用0补全,这些存储空间都浪费了,本来 2个字节就够了,但是现在要三个了,多了50%空间。

UTF-32 的编码方式,也就是上述那种定长编码,字符统一使用 4 个字节.

方案2 UTF8

采用 不定长的方式

这里我就着重介绍一下,现在的最常用的UTF-8, 另外提一嘴,UTF-8是不兼容GBK的,用UTF-8编码的方式打开 绝对会乱码.

UTF-8编码字符理论上可以最多到6个字节长,UTF-8 不但兼容了 ASCII ,而且可以做到不定长.下面看一下 具体原理.

  • 对于单个字节的字符,第一位设为 0,后面的 7 位对应这个字符的 Unicode 码点。因此,对于英文中的 0 - 127 号字符,与 ASCII 码完全相同。这意味着 ASCII 码那个年代的文档用 UTF-8 编码打开完全没有问题
  • 对于需要使用 N 个字节来表示的字符(N > 1),第一个字节的前 N 位都设为 1,第 N + 1 位设为 0,剩余的 N - 1 个字节的前两位都设位 10,剩下的二进制位则使用这个字符的 Unicode 码点来填充

这两端话实在太绕口,请看下表:

占用字节UTF-8 二进制
10xxxxxxx
2110xxxxx 10xxxxxx
31110xxxx 10xxxxxx 10xxxxxx
411110xxx 10xxxxxx 10xxxxxx 10xxxxxx
5111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
61111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

上表中 X的符号都是来存储Unicode 码点,对于 基本平面(BMP) ,最大需要 16bit的空间,对照上表,也就是最大需要3字节的UTF-8编码,最小一个字节,对于汉字需要 3 字节UTF8编码。这也是**UTF8的一点小瑕疵,存储同样的汉字,体积比GBK要大50%。**但是人家可以表示世界大多数字符,还要啥自行车。

UTF8解决字符间分隔的方式是数二进制中最高位连续1的个数来决定这个字是几字节编码

对于一个字节,那就0开头,和ASCII码重合,做到了兼容。

对于多字节为例(以三字节为例),读取到有3个连续的1,后面再跟一个0,说明包括本字节在内,接下来三个字节一起构成了一个文字,除了本字节都是10开头,来标记属于本字符**。其他没有使用到的空间 (16个)X**,用于存储 Unicode编码(码点),拼到一起就构成了 一个字符。

这种巧妙设计,把Unicode的数值和每个字的字节数融合在一起,就算最坏情况是6个字节表示一个字(一共25bit来存储Unicode),已经足够表示世界上所有语言的所有文字了。UTF8和GBK没有任何关系,除了都兼容ASCII以外。

字为例

字符
UTF-8(16进制)E8 B6 8A
UTF-8(2进制)1110 1000 1011 0110 1000 1010
Unicode 编码(16进制)8D8A
Unicode 编码(2进制)1000 1101 1000 1010

看表就明白了(看2进制),不在赘述。

UTF-16

这里 部分摘抄 Li Yucang 博客 彻底弄懂Unicode编码

C / C++ 中遇到的 wchar_t 类型或 Java 中的 char 类型等等,规定的是 都是2个字节(wchar_t windows 规定2字节,Linux 4字节),Unicode 基本平面就是 也就是 2字节就够了,因此两个字节几乎可以覆盖大部分的常用字符

我目前认为 wchar_t windows上是utf-16,在linux上实现是utf-32,但是没有找到确切的资料,不敢确定。

UTF-16 也是一个不定长的字符编码方式,长度编号就两种 4or2 字节

基本平面的字符占用 2 个字节,辅助平面的字符占用 4 个字节

当我们遇到两个字节时,到底是把这两个字节当作一个字符还是与后面的两个字节一起当作一个字符呢?

这里有一个很巧妙的地方,在基本平面内,从 U+D800 到 U+DFFF 是一个空段,即这些码点不对应任何字符。因此,这个空段可以用来映射辅助平面的字符。

辅助平面的字符位共有 220 个,因此表示这些字符至少需要 20 个二进制位。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 到 U+DBFF(空间大小 210),称为高位(H),后 10 位映射在 U+DC00 到 U+DFFF(空间大小 2^10),称为低位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。

这里就不举例子了。

总结

  • utf-8 存储空间大小可变 1字节 到6字节
  • utf-16 存储空间也可也变 4or2 字节
  • utf-32 每个字符都是 4字节。
  • 对于中文来说 ,utf-8 需要6字节,utf-16需要两字节
  • 对于 辅助平面(缩写 SMP) UTF-8 UTF-16 都要四字节
  • Unicode编码 是没有“长度”这一说的,它是抽象的字符,只有Unicode通过不同的编码方式才有具体的字节长度
  • UTF-16 最少占用 2字节,不兼容 ASCII

补充

UTF-8与UTF-8 BOM

BOM即byte order mark,UTF-8文件中放置BOM主要是微软的习惯,但是放在别的系统上会出现问题。不含BOM的UTF-8才是标准形式,UTF-8不需要BOM带BOM的UTF-8文件的开头会有U+FEFF,所以我新建的空文件会有3字节的大小。

UTF-16 le or be

  • UTF-16BE,其后缀是 BE 即 big-endian,大端的意思。大端就是将高位的字节放在低地址表示。
  • UTF-16LE,其后缀是 LE 即 little-endian,小端的意思。小端就是将高位的字节放在高地址表示。
  • UTF-16,没有指定后缀,即不知道其是大小端,所以其开始的两个字节表示该字节数组是大端还是小端。即FE FF表示大端,FF FE表示小端。

UTF-8mb3 UTF-8mb4

其实这里涉及一个非常有趣的历史,大家只要搜索 UTF-8mb4 就会发现有许多大佬吐槽MySQL ,ahhh.

mb4是指most bytes 4 ,也就是最多存储4个字节的utf-8编码。

至于为啥有 UTF-8mb3 ,应为 MySQL 认为 UTF-8 最高需要6字节,太浪费了,后面大家都用 Emoji 表情 了, Emoji 表情的UTF-8四个字节编码,所以不够了,后面就推出了 UTF-8mb4。

关于UTF-8的思考

由于UTF-8存储一个字符需要的空间不一样,那么定义存储UTF-8的数据类型就要妥协,要么就和MySQL一样 就存储 UTF-8四个字节的编码,其他的都不要了。但是这样就无法体现UTF-8长度不定的特性了。

个人认为,保存文本文件来说 UTF-8 非常完美,长度不定,节省一定的空间,并且兼容ASCII. 但是对一个编程语言来说,长度不定的数据类型? 真的存在吗?

vs2019 里的多字节字符集

多字节和Unicode

多字节字符集,其实就是字面意思,它表示一个字符时可能是一个字节也可能是多个字节。使用的编码也就是 前面提到的ANSI(中文的操作系统也是就是使用 GBK), 中文用着两个char存储起来,英文一个字符。

中文字符可以存储在 两个char中,但是需要两个char连续输出,才可以读取中文字符。否则乱码。

Unicode字符集,就是使用 UTF-16 (默认是UTF-16).

Winodows API有两种,一种W结尾,一种A结尾。

  • W结尾API,对应Unicode字符集。
  • A结尾API,对应ANSI多字节字符集。

char与wchar_t的区别 我前面提到了,char就是来存储多字节字符的,wchar_t(宽字符)就是存储 Unicode字符的。

L"" 和 T("")

注意!:使用常量给 wchar_t存储字符的时候需要 需要在常量前面加L ,例如 L"越行勤"

_T("") _T 是一个宏,如果你编译环境是ANSI ,那么就不起作用,如果是 Unicode ,其和L""一样的作用 ,而L"" 管你支不支持都是 Unicode.

TCHAR

TCHAR 就是当你的字符设置为什么就是什么

  • 程序编译为 ANSI, TCHAR 就是相当于 CHAR
  • 当程序编译为 UNICODE, TCHAR 就相当于 WCHAR

LPWSTR LPSTR LPCSTR LPCWSTR

这里的L 表示 long指针 ,这是为了兼容Windows 3.1等16位操作系统遗留下来的,在win32中以及其他的32位操作系统中, long指针和near指针及far修饰符都是为了兼容的作用。没有实际意义(个人感觉)

  1. LPWSTR(L长,P指针,W宽字符,STR字符串) :相当于 wchar_t* ,一个指向以NULL结尾 的 wchar_t 字符串。
  2. LPSTR : 相当于 char* ,一个指向以NULL(\0)结尾 的 wchar_t 字符串
  3. LPCWSTR 多了个C ,表示 const ,相当于 const wchar_t*
  4. LPCSTR : 多了个C ,表示 const ,相当于 const char*

努力成长的程序员