之前编写win32程序时没怎么关注过宽字符到底是个啥东西,最近在编写网络框架又遇到字符相关的问题,所以写一篇文章记录一下(有些部分属于个人理解,如果有错误欢迎指出)
目录
- 几个常见的编码方式
- Unicode和UTF-8、UTF-16、UTF-32的关系
- 字符编码的应用
- 文本文件里的字符
- 编程中的字符
- 字符串字面量
- 高级语言的窄字符
几个常见的编码方式
- GB2312:早期的汉字编码,覆盖6000+个常用汉字,无法处理生僻字或者古文
- GBK:收录20000+个汉字和符号,包括繁体字生僻字等
- GB18030:较新的汉字字集,与 GB 2312-1980 和 GBK 兼容,共收录汉字 70000+ 个,采用多字节编码,每个字可以由 1 个、2 个或 4 个字节组成
GB2312和GBK均属于2字节定长编码,和ASCII混编,ASCII区字符固定占用1字节,汉字区字符固定占用2字节 - Unicode:为世界上所有字符都分配了一个唯一的数字编号,目前编号范围从 0x000000 到 0x10FFFF,一共17个平面,每个平面有65536个码点
Unicode和UTF-8、UTF-16、UTF-32的关系
Unicode只规定了每个字符的编号,但没有规定二进制码如何存储,而UTF-8、UTF-16、UTF-32就是Unicode的二进制存储实现方案
- UTF-8:使用变长编码,编号小的使用的字节就少,编号大的使用的字节就多。使用的字节个数从 1 到 4 个不等,实现了对 ASCII 码的向后兼容,网络传输一般选择这种方式节省网络资源。编码规则:
- 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
- 对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
- 例如在
U+0080
到U+07FF
之间的字符,UTF - 8 用 2 个字节来编码。其格式为110xxxxx 10xxxxxx
。编码时,将 Unicode 码点的二进制表示分为两部分,前 5 位放在第一个字节的xxxxx
位置,后 6 位放在第二个字节的xxxxxx
位置。
- UTF-16:使用变长编码
- 对于编号在
U+0000
到U+FFFF
的字符(常用字符集),直接用两个字节表示。 - 编号在
U+10000
到U+10FFFF
之间的字符,需要用四个字节表示。 - UTF-16有字节的顺序问题(大小端),所以就有 UTF-16BE 表示大端,UTF-16LE 表示小端。
- 会在字符开头添加
FEFF
(不知道干什么用,网上也找不到资料)
- 对于编号在
- UTF-32:定长编码,直接将码点转换为4字节的二进制表示形式,消耗较大,用得比较少,同样也有字节顺序问题
字符编码的应用
文本文件里的字符
- ANSI:在一些文本编辑器里(例如记事本)会看到ANSI编码方式。ANSI并不是某一种特定的字符编码,而是在不同的系统中,ANSI表示不同的编码,例如中国的计算机ANSI编码即为GBK,美国的计算机ANSI编码即为ASCII
如下图所示, 记事本和rider编辑器右下角都会有显示此文件的编码方式
在这些文本编辑器里选择转换编码方式的话,则文本的内容不变,改变编码方式。选择重新加载,则内存内容不变,重新以新的编码方式解析成文本
下面展示分别使用ANSI(GBK),UTF-8,UTF-16,UTF-32四种方式存储“你好我是123456”这个字符串所需大小:
- ANSI(GBK):汉字占2字节,ASCII字符占1字节
- UTF-8:汉字占3字节,ASCII字符占1字节
- UTF-16:汉字占2字节,ASCII字符占2字节,UTF-16还有在开头添加的2字节
- UTF-32:每个字符4字节,因为记事本没有UTF32,我在Rider修改编码方式
用VS打断点测试一下大端序UTF-16的内存情况,下图为文本文件存储的22字节内容:
除了开头添加的FEFF,“你好我是”这四个汉字的Unicode编号分别为
可以发现和内存里汉字的内容和字符编码相对应,后面的3100
,3200
这些就不一个个查了
编程中的字符
- C/C++中窄字符和宽字符共用,窄字符即
string(char)
,宽字符即wstring(wchar_t)
,Python/C#这种高级语言里的string默认为宽字符 - 窄字符字符串以单个字节为单位,输出长度是输出字节数;宽字符字符串以字符为单位,输出长度是输出字符数
字符串字面量
C++中存储窄字符串字面量的话,字符串编码方式和代码文本文件的编码方式有关,存储宽字符串字面量的话要在字符串前面加‘L’标记,在Python/C#之类的高级语言里则是直接存储宽字符的字面量
例如下面这一段C++代码,如果文件格式为GBK,则输出14,如果为UTF-8,则输出18
#include <iostream>
int main()
{
std::string str = "你好我是123456";
std::cout << str.size();
return 0;
}
下面是C++,Python,C#代码分别存储宽字符并输出大小的代码:
#include <iostream>
int main()
{
std::wstring str = L"你好我是123456";
std::cout << str.size();
return 0;
}
s = "你好我是123456"
print(len(s))
using UnityEngine;
public class StartUp : MonoBehaviour
{
private void Start()
{
string str = "你好我是123456";
Debug.Log(str.Length);
}
}
高级语言的窄字符
刚刚说了,Python/C#这种高级语言的字符串默认采用宽字符,如果要在高级语言使用窄字符,则需要对字符串使用encode之类的函数变成高级语言的byte数组
print(len("你好我是123456".encode("utf-8")))
print(len("你好我是123456".encode("gbk")))
using System.Text;
using UnityEngine;
public class StartUp : MonoBehaviour
{
private void Start()
{
string str = "你好我是123456";
byte[] utf8Bytes = Encoding.GetEncoding("UTF-8").GetBytes(str);
byte[] gbkBytes = Encoding.GetEncoding("GBK").GetBytes(str);
Debug.Log("UTF-8编码后的长度" + utf8Bytes.Length);
Debug.Log("GBK编码后的长度" + gbkBytes.Length);
}
}