ASCII
GB2312
GBK
GB18030
BIG5
BIG5-HKSCS
Unicode
ISO-8859-1 Latin1
ISO/IEC 10646 UCS
Hex
ASCII
EASCII
EUC-CN
UTF-8
UTF-16
UTF-32
UCS-2
UCS-4
Base64
UrlEncode
反码 和 补码 的出现主要是为方便负数的二进制计算。
八进制或十六进制缩短了二进制数,但保持了二进制数的表达特点。
计算机里最小的单位,只有两个值,1 和 0;一般缩写为小写 b ;二进制数字(binary digit)的缩写
8 bit 等于 1 byte;一般缩写为大写 B
一连串的位(比特)
一连串的字节
cpu 一次能处理的位串,称为一个计算机字,简称字。 字长(word size),一个 word 的位数,通常是 2 的倍数,例如 16位,32位。 因为英特尔的术语里,一个 word 通常是 16 位。 因为 8086 的字长是 16 位的,后续的处理器为了兼容 8086 也是把一个 word 定义为 16 位。 如果要表达大于 16 位的字长时。通常会使用 dword(double word, 32位) qword(quadruple word, 64位) dqword(double quadruple word, 128位) 。 字(word)这个概念其实和字符编码的关系有点远,这个通常是硬件的概念。 但因为很容易和字符编码的相关概念混淆,所以这里也记录一下。 btw: 二十世纪九十年代时,游戏机里提及的 8 位游戏机、 16 位游戏机、 32 位游戏机,指的就是字长。
在这里 字/字符 代表就是形式上的汉字或英文字母,一个字/字符就代表一个汉字或一个英文字母;一般缩写为 char
一连串的字符, 就是多个字符(char),一个字符也可以作为一个字符串
没有转义字符的字符串
字符串 例子
字符串\n例子
文本 例子
文本
例子
纯文本就是没有用于描述格式的字符的文本,又或者即使有用于描述格式的字符,也不渲染格式只输出字符
例子
<p>这是文本</p>
这是纯文本
字面上的理解就是字符的集合,是一个自然语言文字系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括文字、数字、字母、音节、标点符号、图形符号等。计算机系统中提到的字符集准确地来说,指的是已编号的字符的有序集合(但不一定是连续的)。
一个字符集里的字符转换成二进制数据的规则。
编码作为名词时,
有时是指编码规则,
有时是指一个字符里在某个编码规则里对应的二进制数字。
例如,拉丁字母 A
在 ascii 里所对应的编码是 01000001 ,汉字 一
在 utf-8 里所对应的编码是 11100100 10111000 10000000 。
编码作为动词时是指把字符转换为二进制数据的过程,又或者是一种二进制数据转换成另一种二进制数据的过程(例如 base64 的编码)。
编码具体的意思还是要看具体的语境。
可以简单地理解为 字符集 + 编码规则
定长编码,就是指一个编码里,每个字符的位数都是相同的,例如 ascii 里每个字符都是 7 位, utf-32 里每个字符都是 32 位。 变长编码,就是指一个编码里,字符的位数可以不相同,例如,在 utf-8 里, ascii 部分的字符都是一个字节,但一些不常用的字符,可以是四字节甚至是六字节。
单字节编码,就是指一个编码里,每个字符都是一个字节。例如, ascii 双字节编码,就是指一个编码里,每个字符都是两个字节的。例如, UCS-2 多字节编码,就是指一个编码里,单个字符的字节可能会多于两个。例如, utf-8 。在 utf-8 , utf-32 这类字符能多于两个字节的编码出现之前,双字节编码也会被称为多字节编码。
ASCII 是 ANSI(美国国家标准学会)制定的一套编码标准。 是应用最广泛的编码,大部分的编码都能兼容 ASCII 。 CP437 是 windows 里的代码页,基本和 ASCII 一致,但一些控制字符被替换成了其它能显示的字符。
ANSI 编码是 ANSI(美国国家标准学会)制定的一套编码草案,该草案最终成为 ISO 8859-1 ,正式标准 ISO 8859-1 和 ANSI 编码草案不完全相同。 ANSI 编码在 windows 的代码页为 cp1252 ,但 cp1252 和 ANSI 编码草案不完全相同。 cp1252 在 ISO 8859-1 定稿之前实施,所以和 ISO 8859-1 也有一点不一样。 在 windows 系统里 ANSI 编码一般是指本地编码,如果语言设为英语, ANSI 就是 cp1252 ,如果语言设为中文,ANSI 就是 GBK
这一时期字符集和编码没有区分,ASCII 只支持英文,使用 7 位代表一个字符,一个字符占一个字节,最高位为 0,多余的一位没有作用。 只定义了 128 个字符(2^7=128)
因为 ASCII 只支持英文,同时为了保证前向兼容,所以其它国家在 ASCII 的基础上作出各自的拓展。一般的拓展都是把 ASCII 中的最高位利用起,这种兼容 ASCII 的字符编码那时会被成为 EASCII (Extended ASCII)。 ASCII 拓展的字符集中比较有影响的是 ISO 8859-1,这是拉丁字母的拓展,基本覆盖西欧各国的字母,所以也被称为 latin-1,这个字符集也是 JAVA 的默认字符集。ISO 8859 除了 lation-1 之外还有 14 个字符集,用来表示欧洲各个国家的文字。 因为 ISO 10646 的出现和发展,ISO 8859 现在已经停止开发。
而汉字因为字符非常多,所以即使用了 ASCII 最高位也无法表示全部汉字,为了尽可能多地收录汉字,就出现了两个字节代表一个字符的字符集,例如 GB2312,BIG-5,这些字符集通常被成为双字节字符集(DBCS,Double Byte Character Set)或多字节字符集(Multi Byte Character Set)。
笔者认为 ISO 8859-1 的制定是 ASCII 时期向本地化时期过渡的标志。在本地化时期,字符集和编码开始分离,但一个字符集几乎只有一个编码,所以这个时期字符编码仍是被放在一起的。
在本地化时期出现的各种 ASCII 拓展,绝大部分是互不兼容的,为了使国际间信息交流更加方便,于是由 Xerox、Apple 等软件制造商于1988年组成的统一码联盟。统一码联盟制定了 Unicode,这一能表示几乎全部字符的字符集。Unicode 定义了一个现代化的字符编码模型,把字符和编码解耦了。Unicode 是一个字符集,而实现这个字符集的编码有三种,UTF-16,UTF-32,UTF-8。刚开始时,Unicode 只有 UTF-16,这一双字节的编码,但后来发现,双字节容量仍不够大,于是就在双字节的基础上翻一倍,出现了四字节的编码,也就是 UTF-32。UTF-8 是一种可变长的编码,可以使用一到六个字节来表示一个字符,例如,兼容 ASCII 部分就是使用一个字节,常用汉字就使用两个字节,一些生僻的字符就是用四个字节或六个字节。UTF-8 是当下使用最广泛的一种编码。UTF 后面跟着的数字是代表这个编码里最少可以使用多少位来表示一个字符。
笔者认为 Unicode 的制定是本地化时期向国际化时期过渡的标志。从 Unicode 开始,字符集和编码被准确地划分。
现在比较流行的中文字符集大概有五种(GB2312,GBK,GB18030,BIG5,BIG5-HKSCS),以及包含中文的 Unicode 。
GB2312 只包含常用的 6000多个常用简体汉字和 ascii 码,除了一些老掉牙的网站基本和一些对性能有极端要求的单片机,基本没地方在用了。
GB13000 93年发布,字符集大概等同于Unicode 1.1.1 ,编码大概等同于 usc2 。GB13000 包含 20902 个汉字,但因为不兼容 ascii 和 GB2312 所以应用得比较少。
GBK 是对 GB2312 的拓展,GBK 能兼容 GB2312,据说 GBK 是 guo(国) biao(标) kuo(扩) 的缩写。包含了更多的汉字,也收录了一部分的繁体汉字。 GBK 能兼容 GB2312。windows 的系统语言设为中文,那么 系统里的 ASNI 编码就是 GBK 。 GBK 不是国家标准,只是技术规范指导性文件,但后续的 GB18030 兼容 GBK 而不是 GB13000 。 GBK 收录的汉字比 GB1300 多,但 GBK 没有收录彦文。
有说是微软在GB2312的基础上扩展制订了GBK,然后GBK才成为“国家标准”(也有说GBK不是国家标准,只是“技术规范指导性文件”);但网上也有资料说是先有GBK(由全国信息技术标准化技术委员会于1995年12月1日制定),然后微软才在其内部所用的CP936代码页中以GBK为参考进行了扩展。
关于 GBK 的来历,中文互联网上大概有两种说法, 一种是微软先在 GB2312 上扩展了,然后才有 GBK 的指导文件。 另一种是, GBK 先发布,然后微软才在 Windows95 里使用。
笔者在网上搜索了一下相关的资料,并列了一个时间线。
GB18030 是对 GBK 的拓展, GB18030 能兼容 GBK 。同样地收录了更多的汉字,常用的繁体字基本也收录完了,还收录了一些少数民族的文字。因为 utf8 的广泛使用,这个字符集也用得比较少。
GB2312 虽然是双字节编码,但却也兼容 ascii ,所以 ascii 的字符仍然是一个字节的。在 GB2312 的 ascii 的字符会被称为半角。但 GB2312 里还有一套完整的双字节的英文字符和符号,这些双字节的英文字符和符号会被称为全角。通常情况下,半角字符只占全角字符的一半宽度。据说,全角字符的出现是为了让中英排版时好看一些。
BIG5 (大五码) 是台湾人搞的中文字符集,收录的字数比 GB2312 多,但没有简体字,在大陆这边几乎没用。
BIG5-HKSCS (Hong Kong Supplementary Character Set, 香港增补字符集) 是香港人基于 BIG5 搞的一套字符集,就是在 BIG5 基础上加上一些粤语字和一部分简体字。
BIG5 的由来
BIG5 的乱码和冲码问题
中文资讯交换码(Chinese Character Code for Information Interchange,简称CCCII)
中文标准交换码(CSIC, Chinese Standard Interchange Code),编号CNS 11643,旧名国家标准中文交换码(CISCII, Chinese Ideographic Standard Code for Information Interchange)
十六进制数既可通过添加后缀 H 来表示,也可通过添加前缀 0x 来表示。
为什么要区分 区位码 国标码 和 内码,笔者没在互联网上找到确切的答案。 笔者猜测可能和 EUC-CN 以及 iso 2022 有关, 也可能和二十世纪八十年代,大量出现的中文输入法有关, 也可能是为了和 ascii 兼容,忽略和 ascii 重叠的编号。 <!-- 确实和 EUC-CN 以及 iso 2022 有关 gb 的内码大概对应 big5 gb 的交换码大概对应 CCCII 或 CNS 11643
ISO 2022-CN 是交换码 EUC-CN 是机内码 EUC-TW 是机内码,是 CNS 11643 的机内码
国家标准 | ISO 2022 | EUC |
---|---|---|
GB2312 | ISO 2022-CN | EUC-CN |
JIS X 0208 | ISO 2022-JP | EUC-JP |
KS X 1001 | ISO 2022-KR | EUC-KR |
W3C的编码技术指南规定,应将gb2312字节流视为GBK编码,与GB18030一并使用同一解码器解码。
区位码和交换码好像并没有多少实际的应用 平时遇到的 gb2312 都是机内码,所以很多时候,查看字符编码时 gb2312 会直接显示成 EUC-CN
EUC Extended Unix Code
gb-2312 除了 EUC-CN 之外还有一种名为 HZ 的编码 斯坦福的李楓峰 1989 搞了 HZ 编码方式用在 Usenet 和 Email 上。但 EUC-CN 成了主流,今天我们说 GB2312 编码,基本上就是在说 EUC-CN 编码的 GB2312 字符 HZ-GB-2312 HZ HanZi 汉字 https://cloud.tencent.com/developer/article/1467724
据说 gb系列的区位码就是参考了这个标准 ECMA-35 这种排列管理方式源于 1971 年制定的 ECMA-35 标准,被称为 code page
ECMA-35 就是后来的 ISO 2022 这个标准规定了两种扩充ASCII以支持扩展字符集的方式。 一种是7位编码,用一串特殊字符来在ASCII和扩展字符集之间切换。 这种方式现在已经没人用了,但其实还是有残留支持。 比如在浏览器地址栏打开data:text/html;charset=iso-2022,ABCD %1b%28I@ABCD,会显示“ABCD タチツテト”。 这里%1b%28I 就起到切换字符集的作用,所以后续的@ABCD被解释成日文字符集JIS X 0208中的“タチツテト”。 另一种是8位编码,用多出的一位来指示扩展字符集。 也就是最高位为0时是ASCII(此时取值是0x00-0x7F),最高位为1时是扩展字符集(此时取值是0x80-0xFF)。 这种在现在仍有广泛使用,甚至UTF-8也借用了这种方式。 (还是以日文字符集为例子,浏览器打开data:text/html;charset=shift-jis,ABCD %C0%C1%C2%C3%C4也会显示“ABCD タチツテト”,这里的%C0%C1%C2%C3%C4就是Shift JIS中的“タチツテト”,而Shift JIS是一个兼容JIS X 0208的编码。)
作者:d41d8c 链接:https://www.zhihu.com/question/21918229/answer/3428251841 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
ECMA-35 Character Code Structure and Extension Techniques 字符编码结构与扩展技术
ECMA-94 -> ISO 8859
ISO 646
rfc1843
rfc1922
rfc3629
https://zhuanlan.zhihu.com/p/22874005
unique/versal/form character encoding 唯一/通用/表格 字符 编码
ASCII American Standard Code for Information Interchange 美国 标准 码 信息 交换 美国信息交换标准码
ANSI American National Standard Institute 美国 国家 标准 学会 美国国家标准学会
-->
ISO 10646 来自国际标准化组织(ISO)。1991年前后,统一码联盟(Unicode)和国际标准化组织(ISO)的参与者都认识到,世界不需要两个不兼容的字符集。于是,它们开始合并双方的工作成果,但字符集依然是独立发布的。
ISO/IEC 10646 全称 Information technology -- Universal Coded Character Set (通用字符集) ,缩写为UCS。UCS 有两套编码, UCS-2 , UCS-4 。 UCS-2 大致等于 utf-16 ,UCS-4 大致等于 utf-32 。大致等于并不完全相等,例如,utf-16 双字节的部分和 UCS-2 基本一致,但 utf-16 辅助平面部分是四字节的编码,这里就和 UCS-2 不一样了。 utf-16 辅助平面 通常被称为增补字符。
汉字 一 的 Unicode 下各个字符集的编码
编码 | hex | dec (bytes) | dec | binary |
---|---|---|---|---|
UTF-8 | E4 B8 80 | 228 184 128 | 14989440 | 11100100 10111000 10000000 |
UTF-16BE | 4E 00 | 78 0 | 19968 | 01001110 00000000 |
UTF-16LE | 00 4E | 0 78 | 78 | 00000000 01001110 |
UTF-32BE | 00 00 4E 00 | 0 0 78 0 | 19968 | 00000000 00000000 01001110 00000000 |
UTF-32LE | 00 4E 00 00 | 0 78 0 0 | 5111808 | 00000000 01001110 00000000 00000000 |
Unicode 编号,一般是指 utf-32 BE 编码去掉前导 0 的部分,例如 汉字 一 的 Unicode 编号就是 4E00 。
更多的情况下 Unicode 会写成 hex 的形式 E4 B8 80 。
在不同的编程语言里可能会写成 \u4E00
\4E00
U+4E00
%u4E00
一
一
\xe4\xb8\x80
%e4%b8%80
在 Windows 系统下,按住 alt ,然后键入 Unicode 编号的十进制,就能输入对应的字符。
可以在这个网站查询字符对应的编码 https://unicode-table.com/
<!-- 假设当前终端的编码是 utf-8 echo -n '一' | od -An -tx1 | sed 's/ /\\x/g' echo -e '\xe4\xb8\x80' echo -n '一' | iconv -f UTF-8 -t UTF-16BE | xxd -p | sed 's/^/\\u/' echo -e '\u4e00' echo -n '一二' | iconv -f UTF-8 -t UTF-16BE | xxd -p | sed 's/^/\\u/' 这一句是有效的 echo -n '一二' | awk 'BEGIN { RS = "" } { for (i = 1; i <= length($0); i++) printf("%c\n", substr($0, i, 1)) }' 这几句是无效的 echo -n '一二' | awk 'BEGIN { RS = "" } { for (i = 1; i <= length($0); i++) printf "%c ", sprintf("%c", substr($0, i, 1)) }' echo -n '一二' | awk 'BEGIN { RS = "" } { for (i = 1; i <= length($0); i++) printf "%02x ", sprintf("%d", substr($0, i, 1)) }' echo -n '一二' | awk 'BEGIN { RS = "" } { for (i = 1; i <= length($0); i++) printf("%02x", substr($0, i, 1)) }' 必须要先分割再转换不然会无法准确分割字符 这是有效的,但全部字符都会转换 echo -n '一二三abc' | awk 'BEGIN { RS = "" } { for (i = 1; i <= length($0); i++) printf("%c\n", substr($0, i, 1)) }' | xargs -d "\n" -n 1 -P 1 bash -c 'echo -n $0 | iconv -f UTF-8 -t UTF-16BE | xxd -p | sed s/^/\\\\u/' | sed ':a;N;$!ba;s/\n//g' xxd -p -u 这样就能直接输出大写字母 echo -n '一二三abc' | awk 'BEGIN { RS = "" } { for (i = 1; i <= length($0); i++) printf("%c\n", substr($0, i, 1)) }' | xargs -d "\n" -n 1 -P 1 bash -c 'echo -n $0 | iconv -f UTF-8 -t UTF-16BE | xxd -p -u | sed s/^/\\\\u/' | sed ':a;N;$!ba;s/\n//g' echo -n '一二三abc' | awk 'BEGIN { RS = "" } { for (i = 1; i <= length($0); i++) printf("%c\n", substr($0, i, 1)) }' | xargs -d "\n" -n 1 -P 1 bash -c 'echo -n $0 | iconv -f UTF-8 -t UTF-16BE | od -An -tu4 | sed s/^/\\\\u/' | sed ':a;N;$!ba;s/\n//g' echo -n '一二三abc' | awk 'BEGIN { RS = "" } { for (i = 1; i <= length($0); i++) printf("%c\n", substr($0, i, 1)) }' | xargs -d "\n" -n 1 -P 1 bash -c 'echo -n $0 | iconv -f UTF-8 -t UTF-16LE | od -An -tu4' echo -n '一二三abc' | awk 'BEGIN { RS = "" } { for (i = 1; i <= length($0); i++){printf("%c", substr($0, i, 1))|"iconv -f UTF-8 -t UTF-16LE | od -An -tu2" }}' echo -n '一二三abc' | awk 'BEGIN { RS = "" } { for (i = 1; i <= length($0); i++) printf("%c", substr($0, i, 1))|"iconv -f UTF-8 -t UTF-16BE | od -An -tu1" }' 这是有效的,只转换非ascii字符 echo -n '一二三abc' | awk 'BEGIN { RS = "" } { for (i = 1; i <= length($0); i++) printf("%c\n", substr($0, i, 1)) }' \ | xargs -d "\n" -n 1 -P 1 bash -c 'echo -n $0 | iconv -f UTF-8 -t UTF-16LE | od -An -tu4' \ | awk '{if(length($0)>0){if($0<=127){printf("%c", $0)}else{printf("\\u%x", $0)}}}' 这是有效的,但全部字符都会转换 echo -n '一二三abc' | od -An -tx1 | awk '{printf "%s", $0}' | sed 's/ /\\x/g' echo -n '一二三abc' | od -An -tx1 | sed 's/ /\\x/g' 这是有效的,但全部字符都会转换,且按大写字母输出 echo -n '一二三abc' | od -An -tu1 | awk 'BEGIN { RS = " " } { if(length($0)>0){ printf("\\x%X\n", $0) } }' | sed ':a;N;$!ba;s/\n//g' 这是有效的,但全部字符都会转换,且按小写字母输出 echo -n '一二三abc' | od -An -tu1 | awk 'BEGIN { RS = " " } { if(length($0)>0){ printf("\\x%x\n", $0) } }' | sed ':a;N;$!ba;s/\n//g' 这是有效的,只转换非ascii字符,且按小写字母输出 echo -n '一二三abc' | od -An -tu1 | awk 'BEGIN { RS = " " } {if(length($0)>0){if($0<=127){printf("%c\n", $0)}else{printf("\\x%x\n", $0)}}}' | sed ':a;N;$!ba;s/\n//g' 这是有效的,只转换非ascii字符,且按大写字母输出 echo -n '一二三abc' | od -An -tu1 | awk 'BEGIN { RS = " " } {if(length($0)>0){if($0<=127){printf("%c\n", $0)}else{printf("\\x%X\n", $0)}}}' | sed ':a;N;$!ba;s/\n//g' 这是输出大写字母 printf("\\x%X\n", $0) 这是输出小写字母 printf("\\x%X\n", $0) echo -n 'a' | od -An -tu1 str="hello world" echo "$str" | tr 'a-z' 'A-Z' str="hello world" echo "$str" | awk '{print toupper($0)}' gun awk 中有一个非标准方法可以把字符串转换成数字 strtonum 查看当前终端的编码 echo $LANG echo $LC_CTYPE 或 locale toupper awk中把字符转换为大写的函数 tolower awk中把字符转换为小写的函数 系统 shell 终端 这三个的编码是不一样的吗? 终端要看具体的实现 linux 系统的编码是通过环境变量来确定的,所以系统的编码就是 shell 的编码 LC系列的环境变量有很多,好像是十几个,但通常只需要关注这两个 LC_ALL和LANG 就可以了 这两个环境变量通常在这个文件里设置 /etc/profile 或 /etc/sysconfig/i18n 测试123abc 一123abc二 echo -e '\u4e00123abc\xe4\xba\x8c' function convertcodepoint3($str) { return join('', array_map(function($char) { if (ord($char) <= 127) { return $char; } else { return '\u' . dechex(mb_ord(mb_convert_encoding($char, 'utf-16be', 'UTF-8'), 'utf-16be')); } }, mb_str_split($str, 1, 'UTF-8'))); } echo join('', array_map(function($char) { if (ord($char) <= 127) { return $char; } else { return '\x' . dechex(ord($char)); } }, str_split('一123abc', 1))); 当前输入的编码 要转换成的格式 按字节还是按字符划分 如果是按字符划分则要转换成哪一个编码 转换成哪一种进制 二进制 八进制 十进制 十六进制 英文字母是否需要大写 全部字符都转换还是只转换非ascii字符 前缀后缀 感觉 unicode 系列编码是按字符划分的 utf8和其它编码(gkb,gb18030)是按字节划分的 感觉可以为这些转义的字符串写一个单独的章节了 -->Unicode 字符编码模型分为四个层级(level)
除了以上四个层级外,另外还有两个有用的概念:
模型 | 解释 | 例子 |
---|---|---|
ACR | 抽象的字符 | 汉字 一 |
CCS | 字符的编号 码点 | 汉字 一 的 unicode 十进制编号 19968 |
CEF | 用基本数据类型表示字符 码元 | 汉字 一 的 unicode 二进制编号 4E00 ,通常带有前缀 0 |
CES | 作为字节流的字符 字节流 | 汉字 一 具体的编码,例如 utf-16be 4E00 或 utf-16le 004E 或 utf-8 E4B880 |
TES | 传输编码 | 把汉字 一 具体的编码再转换成 base64 或 urlencode 这类编码 |
一些文章会把字符编码模型分为五层 ACR CCS CEF CES TES
参考 https://www.unicode.org/reports/tr17/
参考 http://www.unicode.org/glossary/
unicode 目前有 17 个平面(plane)。 每个平面有 65536(2^16 或 256^2) 个码点(code point)。 一共有 17*2^16 个码点,大概能表示一百万个字符。 17 个平面 21 位比特就能表示完了。
第一平面称为 0 号平面 或 基础多语言平面(Basic Multilingual Plane, BMP)。 其余的 16 个平面称为 辅助平面 或 补充平面 或 增补平面(Supplementary Plane, SP)。
通常一个平面会以一个 1616 的二维表格表示,其中一个格子表示 256 个字符。 然后每个格子展开后又是一个 1616 的二维表格,最后才是一个格子表示一个字符。
每个平面里,还会划分多个区块(block),每个区块都是一类 文字或符号 。但不是每个区块都刚好是 256 的倍数。 例如 编号 0000—007F 是基础拉丁文(Basic Latin),编号 4E00—9FFF 是 中日韩统一表意字(CJK Unified Ideographs)。
平面 | 起始编号 | 名称 |
---|---|---|
0 号平面 | U+0000 - U+FFFF | 基本多文种平面 (Basic Multilingual Plane,BMP) |
1 号平面 | U+10000 - U+1FFFF | 多文种补充平面 (Supplementary Multilingual Plane,SMP) |
2 号平面 | U+20000 - U+2FFFF | 表意文字补充平面 (Supplementary Ideographic Plane,SIP) |
3 号平面 | U+30000 - U+3FFFF | 表意文字第三平面 (Tertiary Ideographic Plane,TIP) |
4 号平面 至 13号平面 | U+40000 - U+DFFF | (尚未使用) |
14 号平面 | U+E0000 - U+EFFFF | 特别用途补充平面 (Supplementary Special-purpose Plane,SSP) |
15 号平面 | U+F0000 - U+FFFFF | 保留作为私人使用区(A区) (Private Use Area-A,PUA-A) |
16 号平面 | U+100000 - U+10FFFF | 保留作为私人使用区(B区) (Private Use Area-B,PUA-B) |
BMP 里也有一个 PUA 区块,编号 0xE000-0xF8FF 。
14 号辅助平面,目前仅摆放“语言编码标签”和“字形变换选取器”,它们都是控制字符。
在辅助平面的字符,通常会被称为 增补字符 。 在 utf-16 里,这些字符需要用到 4 个字节来表示。
传说, unicode 一开始只是规定了 65536 个码点,所以 utf-16 一开始也是两个字节。 那时的 unicode 可能是觉得 65536 就能表示人类的全部字符了。 但后来发现 65536 个码点,完全不够用,于是又增加了平面的概念。 一开始的 65536 个码点称为基本平面,后续新增的 16 个平面称为辅助平面,每个平面 65536 个码点。 所以,后续的 ucs-4 和 utf-32 都是四个字节,编码空间暂时还是很充裕。
为了让 utf-16 能表示增补平面的字符, 于是 utf-16 增加了代理机制(surrogate)。
在 BMP 里有一个代理区块(Surrogate Zone)专门用于 utf-16 的代理。 代理区有 8 个区块,一共 2048(256*8) 个码点,代理区的范围是 0xD800-0xDFFF 。
代理机制,大概就是用一个代理区码点和一个非代理区码点组成一个代理码元, 两个代理码元表示一个增补平面的字符。 两个码元就是 4 个码点就是 4 个字节。 这两个代理码元会被称为 代理项对(Surrogate Pair) 。
utf-16 的代理对刚好能表示完 16 个增补平面。
大致的代理规则
代理码元1 | 代理码元2 |
---|---|
1101 10pp ppxx xxxx | 1101 11xx xxxx xxxx |
大致的算法
例子
#include <stdio.h>
#include <stdlib.h>
#include <uchar.h>
#include <string.h>
#include <locale.h>
void dump_bytes(char* str, int len)
{
for (int i = 0; i < len; ++i)
{
printf("%p %x\n", str+i, *(str+i));
}
}
void printf_utf16(unsigned int unicode)
{
char buf[5] = {'\0', '\0', '\0', '\0', '\0'};
mbstate_t ps;
memset(&ps, 0, sizeof(ps));
if (unicode < 0x10000) // 0x10000 00010000000000000000 65536
{
dump_bytes((char*)(&unicode), 2);
c16rtomb(buf, (char16_t)unicode, &ps);
printf("%s\n", buf);
}
else
{
unicode = unicode - 0x10000;
char32_t b;
b = (char32_t)0xd800 + (unicode >> 10);
b = b << 16;
b = b | (0xdc00 + (unicode & 0x3ff));
dump_bytes((char*)(&b), 4);
printf("%x\n", b);
}
}
int main()
{
setlocale(LC_ALL, "en_US.UTF-8");
unsigned int utf16_1 = 19968; // 二字节编码的例子
unsigned int utf16_2 = 150370; // 四字节编码的例子
printf_utf16(utf16_1);
printf("\n");
printf_utf16(utf16_2);
printf("\n");
return 0;
}
C 语言的标准库的 c16rtomb 函数并不能处理两个码元的 utf-16 编码 https://en.cppreference.com/w/c/string/multibyte/c16rtomb
在 C11 刚发布时,不同于转换变宽多字节(如 UTF-8 )到变宽 16 位(如 UTF-16 )编码的 mbrtoc16 ,此函数只能转换单个单元的 16 位编码,这表示尽管此函数的原目的如此,它仍不能转换 UTF-16 到 UTF-8 。这为 C11 后的缺陷报告 DR488 所更正。
UTF-8 的编码规则很简单,有二条:
unicode 符号范围(十进制) | unicode 符号范围(十六进制) | utf-8 编码方式 |
---|---|---|
0 - 127 | 0x00 - 0x7f | 0xxxxxxx |
128 - 2047 | 0x80 - 0x7ff | 110xxxxx 10xxxxxx |
2048 - 65535 | 0x800 - 0xffff | 1110xxxx 10xxxxxx 10xxxxxx |
65536 - 2097151 | 0x10000 - 0x1fffff | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
2097152 - 67108863 | 0x200000 - 0x3ffffff | 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
67108864 - 2147483647 | 0x4000000 - 0x7fffffff | 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
utf-8 实际上最多只能使用 31 位和 utf-32 相比还少了 1 位,但和 17 个平面的 21 位相比还是很宽裕的。
下面是一个简单的,把 unicode 转换成 utf-8 的例子,但不考虑超过 4 字节的 utf-8 ,因为 4 字节的 utf-8 足够表示 17 个平面的字符了。
基本的流程就是
#include <stdio.h>
void printf_utf8(unsigned int unicode)
{
char str[5] = {'\0', '\0', '\0', '\0', '\0'};
if (unicode <= 0x7f) // 0x7f 01111111 127
{
str[0] = (char)unicode;
}
else if (unicode >= 0x80 && unicode <= 0x7ff)
{
str[0] = 0xc0 | ((unicode >> 6) & 0x1f); // 0xc0 11000000 0x1f 00011111
str[1] = 0x80 | (unicode & 0x3f); // 0x80 10000000 0x3f 00111111
}
else if (unicode >= 0x800 && unicode <= 0xffff)
{
str[0] = 0xe0 | ((unicode >> (6 * 2)) & 0x0f); // 0xe0 11100000 0x0f 00001111
str[1] = 0x80 | ((unicode >> 6) & 0x3f);
str[2] = 0x80 | (unicode & 0x3f);
}
else if (unicode >= 0x10000 && unicode <= 0x10ffff)
{
str[0] = 0xf0 | ((unicode >> (6 * 3)) & 0x07); // 0xf0 11110000 0x07 00000111
str[1] = 0x80 | ((unicode >> (6 * 2)) & 0x3f);
str[2] = 0x80 | ((unicode >> 6) & 0x3f);
str[3] = 0x80 | (unicode & 0x3f);
}
printf("%s", str);
}
int main()
{
unsigned int utf8_1 = 65; // 和 ascii 码兼容的例子
unsigned int utf8_2 = 415; // 二字节编码的例子
unsigned int utf8_3 = 19968; // 三字节编码的例子
unsigned int utf8_4 = 131954; // 四字节编码的例子
printf_utf8(utf8_1);
printf("\n");
printf_utf8(utf8_2);
printf("\n");
printf_utf8(utf8_3);
printf("\n");
printf_utf8(utf8_4);
return 0;
}
通用区域数据存储库 (Common Locale Data Repository, CLDR)
Unicode 国际组件 (International Components for Unicode, ICU)
UCA
rtl (right-to-left)
bidi
这是一段测试用的文字
CJKUI 中日韩统一表意字
国际表意文字核心(International Ideographs Core,简称 IICore 或易扩)
Unihan
mysql 的 utf8 和 utf8mb3 和 utf8mb4
MySQL utf8mb4 的排序规则
unicode 里关于汉字的问题
emoji 表情
为什么没有 utf-24 https://www.v2ex.com/t/399575
BOM(Byte Order Mark),字节顺序标记,出现在文本文件头部,Unicode 编码标准中用于标识文件是采用哪种格式的编码。 在 UFT-8 编码格式的文本中,如果添加了BOM,则只用它来标示该文本是由 UTF-8 编码方式编码的,而不用来说明字节序,因为 UTF-8 编码不存在字节序问题。
unicode 里有一个名为 零宽度非断空格符 (ZERO WIDTH NO-BREAK SPACE, zwnbsp) 的不可见字符,用于阻止特殊位置的换行分隔。 同时也是用于标识字节序。 通常会作为文本或字节流的开头。
GB2312、GBK、GB18030 都是兼容 ASCII,区分 ASCII 的方法是高字节的最高位为0。 在读取字符流时,只要遇到高位为1的字节,就可以将下两个字节作为一个双字节编码,而不用管低字节的高位是什么。 big5 和 ISO 8859 也是用类似的方式兼容 ASCII 。 所以 GB2312、GBK、GB18030、big5、ISO 8859 是没有 BOM 的问题的。
Hex 是十六进制的意思,一般就是按字节二进制数据转换成十六进制显示,例如 00001100 转换成十六进制 c ,一般会以 0x 开头,所以会写成 0xc 。
echo -e -n "\xe8\x4d\x3a\xa5" | xxd -plain
echo -e -n "\xe8\x4d\x3a\xa5" | od -t x1 -An
echo -e -n "\xe4\xb8\x80"
echo -e -n "\xe4\xb8\x80" | iconv -t UTF-8
base64 就是把二进制数据用 ascii 里的 65 个字符表示,A ~ Z a ~ z 0 ~ 9 + / = 。
echo 123 | base64
echo MTIzCg== | base64 -d
echo 123 | openssl enc -base64 -e
echo MTIzCg== | openssl enc -base64 -d
UrlEncode 类似于 base64 ,也是用 ascii 字符来表示数据,一般用在 url 里的地址部分 或 提交表格的 body 里。
似乎没有命令能直接编码 url
相关的标准 RFC1738 RFC3986
使用 python 实现的,标准输入中一定要有数据,不然会一直等待
编码
echo -n $graphqlquery | python -c "import sys;import urllib.parse;data=sys.stdin.read();print(urllib.parse.quote_plus(data));"
解码
echo -n $graphqlquery | python -c "import sys;import urllib.parse;data=sys.stdin.read();print(urllib.parse.unquote(data));"
python 中的相关方法
quote 不编码保留字符,类似于 js 的 encodeURIComponent
quote_plus 编码保留字符,类似于 js 的 encodeURI
unquote 解码
使用 php 的实现
编码 RFC1738
echo -n $graphqlquery | php -r 'print(urlencode(file_get_contents("php://stdin")));'
解码 RFC1738
echo -n $graphqlquery | php -r 'print(urldecode(file_get_contents("php://stdin")));'
编码 RFC3986
echo -n $graphqlquery | php -r 'print(rawurlencode(file_get_contents("php://stdin")));'
解码 RFC3986
echo -n $graphqlquery | php -r 'print(rawurldecode(file_get_contents("php://stdin")));'
使用 sed 实现的,但没能处理好换行符,只要不介意换行符,这就是能使用的了
编码
echo -n $graphqlquery | tr "\r\n" " " | tr "\n" " " | while IFS=''; read -n 1 c; do echo -n $c | sed -n -r '/[^a-zA-Z0-9_\.~\-]/!b Print;s/(.{1})/bash -c "echo -n \\"\1\\" | xxd -plain "/e;s/([a-zA-Z0-9]{2})/%\0/g;:Print;p'; done;
解码
echo -n $graphqlquery | sed -r -n 's/%([a-zA-Z0-9]{2})/\\x\1/g;s/.*/echo -e -n "\0" /e;p'
punycode 是用于域名里非 ascii 字符的编码,类似于 UrlEncode 。例如,中文域名就是先转换成 punycode 再查询 DNS 的。punycode 就由26个字母+10个数字,还有“-”组成。
base58 就是把二进制数据用 ascii 里的 58 个字符表示,A ~ Z (去除大写字母 O ,大写字母 I) a ~ z (小写字母 l) 1 ~ 9 。 大写字母 O ,大写字母 I ,小写字母 l 数字 0 比较容易混淆。
Base58Check 在 base58 的基础上加上校验机制,主要用于表示 Bitcoin 的钱包地址。
在 Windows 系统中字符编码通常会用代码页(code page, cp)来表示
<!-- 为什么是 cp ? -->Windows 代码页和字符集的对应关系 代码页|字符集|备注 -|-|-| cp 437|IBM437| cp 936|GBK| cp 54936|GB18030| cp 950|BIG5| cp 65001|UTF-8| cp 1252|ISO-8859-1| cp 1200|UTF-16 little endian| cp 1201|UTF-16 big endian| cp 12000|UTF-32 little endian| cp 12001|UTF-32 big endian|
参考 https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
Windows 通常以以下三种格式之一来实现操作字符的 API 函数:
https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/chcp
https://docs.microsoft.com/zh-cn/windows/win32/intl/unicode-and-character-sets
按照 GSM 900/1800/1900 的标准,每条短信最多发送1120位,也就是(1120÷8=140一个字节占8位)140字节的内容。 如果发送纯 ASCII 码字符,ASCII 采用7位编码,所以1120位的限额可以传送1120÷7=160个字符。这里不会像一般的程序那样,一个字符占一字节(8位),空余一个位出来,而是一个字符占7位。 如果发送的内容里含有非 ASCII 字符,就会自动转换为 UCS-2 编码,这时所有字符都采用2个字节的8位编码,所以1120位的限额可以传送1120÷(8*2)=70个字符。 所以,只要短信里含有汉字,那么短信的编码就是 UCS-2 ,所以一条短信最多只能有 70 个汉字
运营商限制单条短信长度70字,但是允许拼接,即多条短信组合成一条短信。发送时根据协议将短信拆分,接收时根据协议将短信合并,这样就可以突破字数限制。
太长就不能称为短信了
字体 = FONT
字库 = 字体库 = FONT LIBRARY
在一些嵌入式系统里,因为可用的内存十分的少,为了存储汉字或其他东亚文字的字形位图,需要一个单独的芯片。 这种单独的芯片通常会被称为字库或字库芯片。 这种芯片往往是只读不可写的。
旧时代的手机也有类似的设计。 在智能手机时代,严谨意义上的字库芯片已经没有了,但依然会把手机里的那块 flash memory 称为字库。
从一个写上层应用的程序员角度来看,字体 == 字库。
字体有很多种格式,但基本上都是根据编码输出对应的字形位图。
字体的渲染过程大致是这样的
字体可以简单但不严谨地理解为一个很大的编码对字形索引的键值对。这个键值对会被称为 Character to Glyph Index Mapping Table 简称 Cmap table 或 cmap 。
现时(2022)的字体格式都是最多只能包含 65535 个字符,这是因为字形索引的长度最多是 16 位。
据说 HarfBuzz4.0 已经突破了 65535 的限制
字体的具体实现其实挺复杂的,可以参考一下 OpenType 的文档
FreeType2 Tutorial 这是一个关于如何实现字体的教程,相当的硬核
一些常见的字体格式
TTF (TrueType Font) 字体格式是由苹果和微软为 PostScript 而开发的字体格式。 在 Mac 和 Windows 操作系统上,TTF 一直是最常见的格式,所有主流浏览器都支持它。
OTF (OpenType Font) 由 TTF 演化而来,是 Adobe 和微软共同努力的结果。 OTF 字体包含一部分屏幕和打印机字体数据。
EOT (Embedded Open Type) 字体是微软设计用来在 Web 上使用的字体。 是一个在网页上试图绕过 TTF 和 OTF 版权的方案。可惜 EOT 格式只有 IE 支持。
WOFF (Web Open Font Format) 本质上是 metadata + 基于 SFNT 的字体(如 TTF、OTF 或其他开放字体格式)。 该格式完全是为了 Web 而创建,由 Mozilla 基金会、微软和 Opera 软件公司合作推出。
WOFF2 是 WOFF 的下一代。 WOFF2 格式在原有的基础上提升了 30% 的压缩率。
一个字节占 8 位,好像没有哪个标准文件里有提及,但事实上又都是这样。
实际上一个字节可以不是 8 位, C 语言标准头文件 <limits.h> 中定有一个宏 CHAR_BIT,用以表示字节位数。 从一个写上层应用的程序员角度来看,一个字节就是 8 位。
IBM在 1950 年设计 IBM 7030 Stretch 的时候引入 byte 的概念,表示程序访问内存的最小单位。 叫 byte 就是为了跟 bit 有所区分。 7030 一个 byte 可能包含 1-bit 到 8-bit 不等,但最多是 8-bit 。 到了 1964 年,IBM 设计出 IBM System/360 大型机,取得重大成功。 而 System/360 的一个 byte 就是 8-bit。 而 System/360 的一个 byte 之所以是 8-bit ,据说是为了兼容打孔卡的数据。
还有一个原因就是, ASCII 一个字符是 7 位,然后加上 1 位校验码,刚好是 8 位。
C 语言标准库中有三套字符串处理函数,分别是
但输入和输出函数只有 字符 和 宽字符 的版本。
对于 ASCII 用普通的字符串处理函数就可以了。 对于 GBK utf-16 utf-32 这种,用 宽字符 版本的函数,但要先设置好 locale 。 对于 utf-8 这种是最麻烦的了,一般是先转换成 utf-32 然后再用 宽字节 版本的函数处理。
在 C 语言中字符通常是指 char 类型。 C 语言中还有其它字符类型。
类型 | 描述 |
---|---|
char | 一个字节 |
char16_t | 16位,两个字节 |
char32_t | 32位,四个字节 |
wchar_t | 一般是两个字节,但其实是通过 locale 的设置决定的。其实就是因为 wchar_t 无法确定具体的字节数,才会有 char16_t 和 char32_t 这两种类型的出现 |
几个容易混淆的符号
null | 一般指空指针,直接使用会提示未定义 |
0 | 是数字0,一般是 int 类型,全部位数都是0 |
'0' | 是字符0,一般是 char 类型,对应的ascii码 48 |
'\0' | 是字符串结束的字符,一般是 char 类型,全部位数都是0 |
"0" | 是字符串,实质是一个字符数组 {'0', '\0'} |
"\0" | 是字符串,实质是一个字符数组 {'\0', '\0'} |
"" | 是字符串,实质是一个字符数组 {'\0'} |
NULL | 宏定义,实质是 ((void*)0),是一个指针 |
'\0' 的意思就是 ascii 的第一个字符, 反斜杠后面跟着的其实就是 ascii 码的八进制数字。
<!-- #include <stdio.h> #include <string.h> int main() { char a[] = {'\0'}; char b[] = {'\0', '\0'}; char c[] = ""; char d[] = "\0"; int length; length=sizeof(a)/sizeof(a[0]); printf("length of a=%d, strlen=%d \n", length, strlen(a)); // 1 0 length=sizeof(b)/sizeof(b[0]); printf("length of b=%d, strlen=%d \n", length, strlen(b)); // 2 0 length=sizeof(c)/sizeof(c[0]); printf("length of c=%d, strlen=%d \n", length, strlen(c)); // 1 0 length=sizeof(d)/sizeof(d[0]); printf("length of d=%d, strlen=%d \n", length, strlen(d)); // 2 0 return 0; } -->对于 char16_t char32_t wchar_t 这类字符的声明,需要在前面加上一个标识的符号
类型 | 符号 | 例子 |
---|---|---|
char16_t | u | char16_t chr = u'中' |
char32_t | U | char32_t chr = U'中' |
wchar_t | L | wchar_t chr = L'中' |
同样地,对应类型的字符串也是需要加上对应的符号
不然会有这种警告的
warning: multi-character character constant [-Wmultichar]
还可以使用转义符 \ 来表示字符,在字符串里同样也适用
格式 | 描述 | 例子 |
---|---|---|
\hhh | ASCII 编码的八进制表示,可以省略前导 0 | \0 \101 |
\xhh | ASCII 编码的十六进制表示,可以省略前导 0 | \x0 \x41 |
\uhhhh | utf-16 be 编码的十六进制表示,不可以省略前导 0 | \u4e2d |
\Uhhhhhhhh | utf-32 be 编码的十六进制表示,不可以省略前导 0 | \U00006587 |
但 ascii 好像不能用 unicode 编码表示,笔者是用 gcc9.2 c17 实践的
这是一个使用转义符来表示字符的例子
#include <stdio.h>
int main()
{
char* ascii = "A\101\x41";
char* unicode = "\u4e2d\U00006587";
printf("%s\n", ascii);
printf("%s\n", unicode);
return 0;
}
输出
AAA
中文
这是一个按字节输出各种类型字符的例子
#include <stdio.h>
#include <wchar.h>
#include <uchar.h>
void dump_bytes(char* str, int len)
{
for (int i = 0; i < len; ++i)
{
printf("%p %x\n", str+i, *(str+i));
}
}
int main()
{
char chr1 = 'a';
wchar_t chr2 = L'中';
char16_t chr3 = u'中';
char32_t chr4 = U'中';
printf("ascii ----------------\n");
dump_bytes((char*)&chr1, sizeof chr1);
printf("wchar_t ----------------\n");
dump_bytes((char*)&chr2, sizeof chr2);
printf("char16_t ----------------\n");
dump_bytes((char*)&chr3, sizeof chr3);
printf("char32_t ----------------\n");
dump_bytes((char*)&chr4, sizeof chr4);
return 0;
}
输出
ascii ----------------
0x7fff8fb4c8d5 61
wchar_t ----------------
0x7fff8fb4c8d8 ffffffad
0x7fff8fb4c8d9 ffffffb8
0x7fff8fb4c8da ffffffe4
0x7fff8fb4c8db 0
char16_t ----------------
0x7fff8fb4c8d6 ffffffad
0x7fff8fb4c8d7 ffffffb8
char32_t ----------------
0x7fff8fb4c8dc ffffffad
0x7fff8fb4c8dd ffffffb8
0x7fff8fb4c8de ffffffe4
0x7fff8fb4c8df 0
wchar_t 是根据 locale 决定的,不同的主机环境可能不一样
在 C 语言中没有字符串类型, 字符串通常是指以 '\0' 结尾的字符数组。 一些多字节编码或变长编码可能会使用结构体来实现。 下文只讨论使用基本类型的字符。
字符串的长度,就是指一个字符串里除了结束标志字符之外的字符个数。 要获得准确的字符串长度,要使用编码对应版本的字符串处理函数。
字符串的大小,就是指一个字符串所占用的字节数。 要获得准确的字符串大小,最好用 sizeof 而不是 strlen 。 strlen 本质上是在计算从开始地址遇到 \0 之前的字节数量。
<!-- 对于 unicode 的编码而言,其实还有两个值, 码点的数量 和 码位的数量 对于 utf-16 而言,其实有一个挺大的坑的,毕竟有 代理机制 -->声明字符数组
char a[] = {'a', 'b'}; // 数组长度是2,这个不是字符串,因为最后一位不是'\0',strlen这时是不可预计的,因为最后一位不是'\0'
char a[] = "ab"; // 数组长度是3,strlen是2,因为会自动补足一位'\0'
char a[] = {"ab"}; // 这个和上面那个一样
char a[] = {'a', 'b', '\0'}; // 这个和上面那个一样
// 这几个和上面是一样的,声明数组时赋值可以忽略长度
char a[2] = {'a', 'b'};
char a[3] = "ab";
char a[3] = {"ab"};
char a[3] = {'a', 'b', '\0'};
// 这种,未赋值的元素会自动初始化为 '\0';
char a[3] = {'a', 'b'};
字符数组和字符指针是不一样的。 字符数组是数组,字符指针是指针,虽然两个都可以当作字符串那样来使用。
字符指针声明后需要初始化才能赋值。
// 正确
char *s = (char*)malloc(6*sizeof(char));
s[0] = 'a';
// 错误
char *s;
s[0] = 'a';
字符指针可以在声明时初始化
char *s = "ab";
可以直接把字符串赋值给字符指针
char *s;
s = "ab";
字符指针可以通过下标来访问
char *s = "ab";
s[0]; // a
*(s+1); // b
直接把字符串赋值字符指针,不能通过下标来修改字符串里的某个字符,但可以整体重新赋值。 之所以不能单独修改某个字符,是因为直接写在代码里的字符串会被存放在数据区里,这部分数据不能被修改。 但可以整体重新赋值,是因为修改的指针指向的地址,相当于只是修改一个变量的值。
// 错误
char *s = "ab";
s[0] = 'a';
// 正确
char *s = "ab";
s = "cd";
这是一个按字节输出各种类型字符串的例子
#include <stdio.h>
#include <wchar.h>
#include <uchar.h>
void dump_bytes(char* str, int len)
{
for (int i = 0; i < len; ++i)
{
printf("%p %x\n", str+i, *(str+i));
}
}
int main()
{
char str1[] = "general string";
wchar_t str2[] = L"这是 wchar_t";
char16_t str3[] = u"这是 char16_t";
char32_t str4[] = U"这是 char32_t";
char str5[] = u8"这是 utf-8";
printf("ascii ----------------\n");
dump_bytes((char*)str1, sizeof str1);
printf("wchar_t ----------------\n");
dump_bytes((char*)str2, sizeof str2);
printf("char16_t ----------------\n");
dump_bytes((char*)str3, sizeof str3);
printf("char32_t ----------------\n");
dump_bytes((char*)str4, sizeof str4);
printf("utf-8 ----------------\n");
dump_bytes((char*)str5, sizeof str5);
return 0;
}
输出
ascii ----------------
0x7ffe009c5401 67
0x7ffe009c5402 65
0x7ffe009c5403 6e
0x7ffe009c5404 65
0x7ffe009c5405 72
0x7ffe009c5406 61
0x7ffe009c5407 6c
0x7ffe009c5408 20
0x7ffe009c5409 73
0x7ffe009c540a 74
0x7ffe009c540b 72
0x7ffe009c540c 69
0x7ffe009c540d 6e
0x7ffe009c540e 67
0x7ffe009c540f 0
wchar_t ----------------
0x7ffe009c5430 ffffffd9
0x7ffe009c5431 ffffff8f
0x7ffe009c5432 0
0x7ffe009c5433 0
0x7ffe009c5434 2f
0x7ffe009c5435 66
0x7ffe009c5436 0
0x7ffe009c5437 0
0x7ffe009c5438 20
0x7ffe009c5439 0
0x7ffe009c543a 0
0x7ffe009c543b 0
0x7ffe009c543c 77
0x7ffe009c543d 0
0x7ffe009c543e 0
0x7ffe009c543f 0
0x7ffe009c5440 63
0x7ffe009c5441 0
0x7ffe009c5442 0
0x7ffe009c5443 0
0x7ffe009c5444 68
0x7ffe009c5445 0
0x7ffe009c5446 0
0x7ffe009c5447 0
0x7ffe009c5448 61
0x7ffe009c5449 0
0x7ffe009c544a 0
0x7ffe009c544b 0
0x7ffe009c544c 72
0x7ffe009c544d 0
0x7ffe009c544e 0
0x7ffe009c544f 0
0x7ffe009c5450 5f
0x7ffe009c5451 0
0x7ffe009c5452 0
0x7ffe009c5453 0
0x7ffe009c5454 74
0x7ffe009c5455 0
0x7ffe009c5456 0
0x7ffe009c5457 0
0x7ffe009c5458 0
0x7ffe009c5459 0
0x7ffe009c545a 0
0x7ffe009c545b 0
char16_t ----------------
0x7ffe009c5410 ffffffd9
0x7ffe009c5411 ffffff8f
0x7ffe009c5412 2f
0x7ffe009c5413 66
0x7ffe009c5414 20
0x7ffe009c5415 0
0x7ffe009c5416 63
0x7ffe009c5417 0
0x7ffe009c5418 68
0x7ffe009c5419 0
0x7ffe009c541a 61
0x7ffe009c541b 0
0x7ffe009c541c 72
0x7ffe009c541d 0
0x7ffe009c541e 31
0x7ffe009c541f 0
0x7ffe009c5420 36
0x7ffe009c5421 0
0x7ffe009c5422 5f
0x7ffe009c5423 0
0x7ffe009c5424 74
0x7ffe009c5425 0
0x7ffe009c5426 0
0x7ffe009c5427 0
char32_t ----------------
0x7ffe009c5460 ffffffd9
0x7ffe009c5461 ffffff8f
0x7ffe009c5462 0
0x7ffe009c5463 0
0x7ffe009c5464 2f
0x7ffe009c5465 66
0x7ffe009c5466 0
0x7ffe009c5467 0
0x7ffe009c5468 20
0x7ffe009c5469 0
0x7ffe009c546a 0
0x7ffe009c546b 0
0x7ffe009c546c 63
0x7ffe009c546d 0
0x7ffe009c546e 0
0x7ffe009c546f 0
0x7ffe009c5470 68
0x7ffe009c5471 0
0x7ffe009c5472 0
0x7ffe009c5473 0
0x7ffe009c5474 61
0x7ffe009c5475 0
0x7ffe009c5476 0
0x7ffe009c5477 0
0x7ffe009c5478 72
0x7ffe009c5479 0
0x7ffe009c547a 0
0x7ffe009c547b 0
0x7ffe009c547c 33
0x7ffe009c547d 0
0x7ffe009c547e 0
0x7ffe009c547f 0
0x7ffe009c5480 32
0x7ffe009c5481 0
0x7ffe009c5482 0
0x7ffe009c5483 0
0x7ffe009c5484 5f
0x7ffe009c5485 0
0x7ffe009c5486 0
0x7ffe009c5487 0
0x7ffe009c5488 74
0x7ffe009c5489 0
0x7ffe009c548a 0
0x7ffe009c548b 0
0x7ffe009c548c 0
0x7ffe009c548d 0
0x7ffe009c548e 0
0x7ffe009c548f 0
utf-8 ----------------
0x7ffe009c53f4 ffffffe8
0x7ffe009c53f5 ffffffbf
0x7ffe009c53f6 ffffff99
0x7ffe009c53f7 ffffffe6
0x7ffe009c53f8 ffffff98
0x7ffe009c53f9 ffffffaf
0x7ffe009c53fa 20
0x7ffe009c53fb 75
0x7ffe009c53fc 74
0x7ffe009c53fd 66
0x7ffe009c53fe 2d
0x7ffe009c53ff 38
0x7ffe009c5400 0
wchar_t 是根据 locale 决定的,不同的主机环境可能不一样
https://zh.cppreference.com/w/c/string/byte
https://zh.cppreference.com/w/c/string/multibyte
https://zh.cppreference.com/w/c/string/wide
https://docs.microsoft.com/zh-cn/cpp/c-runtime-library/string-manipulation-crt?view=msvc-160
C++ 里的字符处理大致和 C 一致。
C++ 里的字符串处理,就比 C 稍微方便一点。
C++ 处理字符串大致有这几种方式
乱码出现的原因通常是程序没有用正确的解码器进行解码和编码。 例如
这里只描述因为编码原因而导致的乱码,这里不讨论因为字体的原因而导致的乱码
一般情况下, windows 的系统语言设为简体中文,那么 cmd 的默认编码是 cp936 也就是 gbk 。
vc 会用一些初值来填充未赋初值或回收后的内存空间, 当这些填充的值按字符输出时,就会按照 cmd 的默认编码来显示字符。
烫 屯 葺 就是这类问题
烫 | CC CC |
屯 | CD CD |
葺 | DD DD |
表现
原因
更详细的解释
utf-8 的 bom 是 零宽度空格 零宽度空格在 utf-8 的编码是 EF BB BF
单独一个的 UTF-8 零宽度空格在 gbk 里会被解释为 锘 。
两个连续的 UTF-8 零宽度空格 EF BB BF EF BB BF , 这 6 个字节在 gbk 里刚好能被解释成三个字符 锘匡豢 。
||| |-|-| |锘|EF BB| |匡|BF EF| |豢|BB BF|
一个网页里会出现多个 utf-8 bom 可能是因为这个网页由多个文件组成,例如 php 或 ssi 里的 include 语法
unicode 里有一个替换字符用于表示无法识别的字符
Replacement Character | � |
Unicode number | 65533 |
UTF-8 | EF BF BD |
UTF-16 | FF FD |
当以 UTF-8 方式读取 GBK 编码的中文时,就会把大量的字符显示为 � , 这时保存文件,就会把原本 GBK 编码的字符替换成 � 。 保存后又用 GBK 格式再次读取, 文件的内容就会被显示为 锟斤拷 。
两个连续的 UTF-8 替换字符 EF BF BD EF BF BD , 这 6 个字节在 gbk 里刚好能被解释成三个字符 锟斤拷 。
锟 | EF BF |
斤 | BD EF |
拷 | BF BD |
「古文码」
鎴戣兘鍚炰笅鐜荤拑鑰屼笉浼よ韩浣撱
「符号码」
巡音ルカ
「拼音码」
ѲÒô¥ë¥«
「符号码」 和 「拼音码」 在 eclipse 里比较常见,因为 eclipse 的默认编码是 ISO8859-1 。
据说新版的 eclipse 已经将默认编码改为 utf-8
"str"
"中文"
u"str"
u"中文"
a.decode("gbk")
b.encode("gbk")
b"byets"
b"中文" # 这样会报错
"byets"
"中文"