C++ 和 Java 的 String 编码问题
什么是字符编码
- 计算机世界只认识0和1,如果想要表示多种多样的字符,需要确定一种01串到字符的映射,比如可以规定"11"代表“我”
- 01串越长,表示的字符越多,比如8位可以表示(1 << 8)个字符,但是每个字符占用的空间也相应变多
- 各种不同的映射规则,就是各种编码标准,其中有几种业界公认的编码标准
字符映射关系有哪些
经典的ASCII编码
ASCII码
- 占8位,但只规定了128个映射
- 包括 10 个阿拉伯数字、26 个大写字母、26 个小写字母、33 个英文标点符、 32 个控制字符,还有最后一个
0x7F
- 将1个字节的最高位的比特规定为 0,使用剩下的 7 个比特,可以完整的表示 ASCII 码规定的字符
由于ASCII的映射上限(1 << 8)较少,很快便有更多映射的需求
Unicode
Unicode解决了ASCII码映射较少的问题,但由于历史原因,仍然出现过位数不够的情况
- 占16位或者32位,16位表示的是基本字符,32位表示的是增补字符,可以表示所有可能得字符
- 仅定义了01串与字符的映射关系,但没有定义如何存储和解析这些二进制编码,可能出现找不到解析头尾的情况
- 也不care存储空间的问题
字符编解码协议有哪些
由于Unicode空间利用率未做优化,也没有通用的编解码协议,函待一种新的编码标准
UTF-8
Unicode 范围(十六进制) | UTF-8 编码方式(二进制) |
---|---|
0000 0000 ~ 0000 007F | 0xxxxxxx |
0000 0080 ~ 0000 07FF | 110xxxxx 10xxxxxx |
0000 0800 ~ 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000 ~ 0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
- 继承了Unicode的映射关系,上图xx部分的大小对应Unicode的大小
- 采用哈夫曼树的优化思路,采用变长节省存储空间
- 采用固定前缀,由此有了编解码协议
以汉字“一”为例,其 Unicode 编码为 0x4E00
,对应的二进制为 100 1110 0000 0000
,二进制共计 15 位。填充到 1110xxxx 10xxxxxx 10xxxxxx
中,最高位缺了一位,使用 0 补齐,最终可得 11100100 10111000 10000000
,即 E4 B8 80
UTF-16
- 2字节或者4字节的编解码协议
- 4字节情况:54开头为4字节的前2字节,55开头为4字节的后2字节
- 2字节情况:其他开头
UTF-32
- 4字节
C++ String编码
-
std::string 就是一个字节数组,与字符编码没有任何关系,只是一个存放数据的容器
-
字符编码编码的字符本质上是用来显示的,所以和存的时候文本采用的编码,和读取的时候采取的编码有关
-
对应如下代码,编辑器采用 sublime,观察下方编码为 UTF-8
#include<bits/stdc++.h> #include <climits> using namespace std; int main() { string myString = "一"; for (std::size_t i = 0; i < myString.size(); ++i) { cout << bitset<8>(myString.c_str()[i]) << endl; } return 0; }
所以结果为,为 UTF-8 编码
11100100 10111000 10000000
Java String编码
-
与C++相反,Java String 是表达字符编码的类,作用是表达字符
-
与C++ 一致,Java String 主要包含 char[] 成员变量
-
与C++相反,Java 的 char 占据两个字节,String 里的 char 一定采用 Unicode,UTF-16 编码协议,无法指定
-
对应如下代码,无论编辑器采用什么编码,Java String 内都会转化为"一"的 UTF-16 编码
String a = "一"; for (char e : a.toCharArray()) { System.out.println("c:" + e + "-> " + Integer.toBinaryString(e)); }
所以结果为,为UTF-16编码
c:一-> 100111000000000 (2字节,并且大小正为 Unicode “一”对应的大小)
-
Java 中 byte[] 可对应 C++ 的 char[],一些转化关系如下
sublime 使用 UTF-8 编码,编写 C++ 代码,得到一个字符串的 UTF-8 编码
#include<bits/stdc++.h> #include <climits> using namespace std; int main() { string info = "$鏱姼䘕¥"; for (std::size_t i = 0; i < info.size(); ++i) { cout << bitset<8>(info.c_str()[i]) << endl; } return 0; }
00100100 11101001 10001111 10110001 11100101 10100111 10111100 11100100 10011000 10010101 11101111 10111111 10100101
对应的编码赋给 Java 的 byte[],并生成String可以表达相同的字符串,但String内部的char却是采用UTF-16编码
byte[] bytes = { (byte) 0b00100100, (byte) 0b11101001, (byte) 0b10001111, (byte) 0b10110001, (byte) 0b11100101, (byte) 0b10100111, (byte) 0b10111100, (byte) 0b11100100, (byte) 0b10011000, (byte) 0b10010101, (byte) 0b11101111, (byte) 0b10111111, (byte) 0b10100101 }; String a = new String(bytes, StandardCharsets.UTF_8); System.out.println("Java String: " + a); for (char e : a.toCharArray()) { System.out.println("c:" + e + "-> " + Integer.toBinaryString(e)); }
Java String: $鏱姼䘕¥ c:$-> 0000000000100100 c:鏱-> 1001001111110001 c:姼-> 101100111111100 c:䘕-> 100011000010101 c:¥-> 1111111111100101
这里涉及到 byte[] 和 String 的转化关系
String str = new String(bytes, StandardCharsets.UTF_8); byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
第一行表示,byte[] 里的字节是 UTF-8 编码的字符,按照 UTF-8 读取,并转化为 UTF-16 存到 String 的 char 里
第二行表示,将 String 的按照 UTF-16 编码的 char,转化为 UTF-8 编码的字节
由此可见, byte[] 只有是 字符编码的字节数组时,转化为String才有意义,否则一定会有bug
-
总结,Java String 表达字符,C++ string 表达01,两者有本质的不同
JNI中存在的问题
由于以上讨论,在JNI的实现上出现了问题
- Java String 表达字符,C++ string 表达01,两者有本质的不同
- 在Java 的 String 转化为 C++ 的 string 时,必然经过由 UTF-16 编码的 Java char 到 其他编码协议编码的 C++ char* 的转化
- 由此,丧失了表达任意01的能力,只能局限在 Java String 表达字符的作用
- 解决方案是 采用 Java 的 byte[] 对齐 C++ 的 char[]