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[]
posted @ 2021-04-26 23:57  Jamgun  阅读(549)  评论(0编辑  收藏  举报