文字与编码的奥秘(下)

本文深入探讨了字符在计算机中的存储原理,从数字编码到字符编码,涵盖了ASCII、GB2312、GBK、GB18030及Unicode等字符集,详细解释了UTF-8、UTF-16和UTF-32编码方式的区别,以及它们在Java中的应用。

在上篇文章中我们已经了解到,计算机内部是采用的二进制进行运算和存储的。通过计算机来代替我们进行日常的工作,必然会遇到如何进行运算以及数据如何进行存储的问题,本篇文章我将和大家一起来了解下文字是如何在计算机中存储的。

说到文字,我们通常联想到的是各种各样的字符:中文、英文、日文、韩文等等。除此之外,就是数字了,这里的数字通常就是指全世界通用的阿拉伯数字。

数字编码

为了简单起见,假设我们定义了一个 无符号 的整型: inti=5; 那计算机拿到这个i之后,他是怎么知道这个变量的值是多少的呢?他又是如何存储的呢?

因为计算机是采用的二进制,而十进制的整数要存储时,首先要先转换成二进制。那么自然而然的就得到了数字编码的过程是:

十进制数字--->二进制数字

例如:数字 5 ,在计算机中的形态就是: 00000101

上面说的是 无符号 的情况, 有符号 的情况更为复杂一些,二进制数字的最高位用作符号位。这时就涉及到另外一种情况了,即:原码,反码和补码。

正数的原码=反码=补码

负数的补码=反码+1

因为用补码存储时既能保证整数又能保证负数的值,所以计算机内部实际是用补码来表示一个数字的。

字符编码

数字编码比较简单,直接将十进制转换成二进制就可以了。但是字符就做不到了,但是我们可以把字符也想象成是一个虚拟的数字,然后再把这个虚拟的数字转换成二进制,不就可以让计算机去处理了吗?

所以字符编码的过程是:

字符------>虚拟数字	
虚拟数字--->二进制数字

那怎么确定字符和虚拟数字之间的关系呢?其实这就是一个 编码 的过程,将每一个单独的字符映射为一个虚拟的数字。当我们把字符映射为数字之后,我们就得到了一个 字符集(Character Set)

我们可以这个字符集想象为一个包含字符与数字之间映射关系的表,这个表有一个名字,叫做 CodePage(码表) ,表中的每一个数字叫做 CodePoint(码点) ,但是这个码点并不是最小的单元,他可能是由一个或多个 CodeUnit(码元) 所组成的。

此外,字符集和字符编码是两个不同的概念,大家需要注意区分,举一个比较容易理解的例子,字符集相当于接口,字符编码相当于实现类。

640?wx_fmt=png

ASCII字符集

因为计算机是美国人发明的,最初设计的码表叫ASCII表。ASCII是American Standard Code for Information Interchange的缩写,他是美国人制定的一套字符编码方案。因为英文中只有52个字母(区分大小写),再加上数字和一些特殊符号和控制字符,总的来说需要编码的字符很少,所以最初的ASCII表中只有128个码点。具体的码表如下图所示:

640?wx_fmt=png

PS:ASCII表中的数字0-9是字符形式的数字,即:"1","2"..."9",和数字的1,2…9是不一样的。

ASCII表中的"1",对应十进制的数字是:49,转换成二进制是 00110001

EASCII字符集

计算机普及后,除了美国人使用之外,很多其他国家的人也开始使用起来。但是原本的ASCII码表已经太小了,所以需要重新找一张大表。最初的ASCII表中只用了一个字节中的7位,最高位是没有使用的,如果把最高位也利用起来的话,就可以多出来128个字符。后来,用人真的把这剩下的128个字符利用了起来,解决了部分西欧语言中的字符的映射。因为这个表是在ASCII表的基础上扩展出来的,所以被称为 EASCII字符集 ,我们经常看到的 ISO 8859-1 的编码方式就是 EASCII字符集 的一种实现。

GB XX字符集

再到后来,计算机传到中国之后,在ASCII码表的基础上,即便预留了128个码点可供选择,重新设计码点。但是对于汉字来说,128实在是太少了,所以我们需要重新造一张表。

GB2312字符集

最先被造出来的表是 GB2312 ,这张表中包含了7445个字符,其中汉字6763个。我们知道一个字节最大表示的范围(不考虑符号位)是0~255,共256个空间,2个字节的最大可表示的范围(不考虑符号位)是0~65535,共65536个空间,显然GB2312用一个字节是表示不全的,至少要用两个字节来表示。

为了与ASCII表兼容,码点在0~127(对应的十六进制是0x00~0x7F)之间的字符与ASCII中保持一致。然后用两个连在一起的字节来表示一个汉字,但是规定第一个字节的范围是0xA1~0xF7,第二个字节的范围是0xA1~0xFE。

说到这里我们就需要了解另外一个概念了: 码元

首先记住一点:码元是组成码点的最小单位。一个码点可能由一个码元组成,也可能由多个码元组成。这取决于不同的编码方式中对码点值的处理方式,稍后我们将在Unicode字符集的编码实现中具体说明这个问题。

GB2312字符集对应的实现方式就是GB2312编码。

GBK字符集

由于GB2312字符集,只收录了6763个汉字,还有好多汉字并未收录,于是微软基于GB2312扩展出了GBK字符集。GBK字符集也是采用的两个字节,第一个字节在0x81~0xFE之间,第二个字节在0x40~0xFE之间,一共收录了两万多个码点,其中汉字有21003个,GBK与GB2312完全兼容。

GBK字符集对应的实现方式就是GBK编码。

GB18030字符集

GB18030字符集与GB2312和GBK基本兼容,但是不同的是GB18030采用变长字节的编码方式,这一点与UTF-8相同。

  • 单字节,从0到0x7F,与ASCII字符集兼容

  • 双字节,第一个字节范围是0x81~0xFE,第二个字节范围是0x40~0xFE,与GBK字符集兼容

  • 四字节,第一个字节范围是0x81~0xFE,第二个字节范围是0x30~0x39,第三个字节范围是0x81~0xFE,第四个字节范围是0x30~0x39

GB18030共收录了70244个汉字。

Unicode字符集

从ASCII字符集开始,后面由不同国家陆续推出了很多不同的字符集,也有各种各样的编码方案。但是,这带来另外一个问题,张三用A字符集编码的结果,李四用B字符集可能解码出来就会出现乱码了,甚至根本解码不出来。因为他们两个所用的码表是不一样的,码点也可能不一样,即便运气好,找到了相同的码点,也有可能解码出来是不同的字符。

那为了解决这种问题,我们就需要一个全世界都认同的大而全的码表,于是Unicode字符集就应运而生了。

由于Unicode字符集太大了,一下子管理不过来,所以在目前Unicode标准中,将字符按照一定的类别划分到0~16这17个平面(Plane层面)中,每个平面中拥有2^16 = 65536个码点。所以,目前Unicode字符集所拥有的码点总数为17*65536=1114112。

Unicode的平面划分,如下图所示:

640?wx_fmt=png

Unicode的码点非常多,但是每个码点最少也需要4个字节,那和传统的ASCII码表就存在不兼容的问题了, 除此以外,如果每个码点都用4个字节来表示的话,就会造成空间的浪费。

UTF-XX编码

为了解决这些问题,就出现了 UTF-XX 这些编码方式,即Unicode码点转换方式(Unicode Transformation Format),一共有三种UTF编码方式,分别是:

  • UTF-8(8-bit Unicode/UCS Transformation Format)

  • UTF-16(16-bit Unicode/UCS Transformation Format)

  • UTF-32(32-bit Unicode/UCS Transformation Format)

其中最简单粗暴的就是UTF-32编码方式,他直接用4个字节来编码每个码点。而UTF-16是用2个字节或4个字节来表示码点的,这将取决于码点在Unicode中哪个Plane中,如果码点在最基本的BMP平面中,那么UTF-16将使用2个字节来编码,否则将使用4个字节来编码。最复杂,最灵活,用的最多的就是UTF-8编码方式了。他可以根据码点的范围使用1到4个字节来编码。

码元和码点

前面我们已经知道了,码点是由一个或多个码元组成的,我们用一个简单的例子来了解下。

640?wx_fmt=png

上图中每一个方框内的都是一个字符,字符下方的是该字符对应的 码点 ,用竖线分隔出来的每个独立的部分是该码点所对应的 码元

  • UTF-32

最简单的就是UTF-32编码方式,他是定长字节的,每个字符都是4个字节,这种方式下的码元是4字节的,每个码点由1个码元组成,并且码点是定长字节的。那么4个字节的码元就可能存在字节序的问题,例如 000003A9 变换字节序之后可能就变成了: 03A90000 ,这时解码就会出现问题。

  • UTF-16

UTF-16编码方式是变长字节的,可以看到有的码点只需要2个字节,有的码点需要4个字节。这种方式下码元是2字节的,每个码点可能由1个码元组成,也可能由2个码元组成,但是不管由几个码元组成,也都会出现字节序的问题。

  • UTF-8

UTF-8编码方式也是变长字节的,从1个字节到4个字节都有,但是他的码元是1个字节。也就意味着UTF-8编码方式不需要考虑字节序的问题。

640?wx_fmt=png

PS:好多人说Unicode编码,这种说法是不准确的,Unicode只是一个字符集,UTF-XX才是他具体的编码方式的实现,不过目前说Unicode编码的说法比较多,通常都把他默认为是UTF-16编码。

字节序

由于UTF-16是2字节码元,一个码点是由两个字节组成的,所以就存在字节序的问题。为了解决这个问题,Unicode规范中引入了一个叫BOM(Byte Order Mark)的东西,即指定这种编码使用哪种字节序来编码,一共有两种BOM:BE和LE,即我们所熟悉的大端序和小端序。

  • 大端序:高位字节在前,低位字节在后

  • 小端序:低位字节在前,高位字节在后

举个例子,汉字“语”用UTF-16编码,大端序的结果是: 8A9E ,小端序的结果是: 9E8A

为什么会有字节序这种奇怪的问题存在呢?这跟计算机的实现有关,我们人类阅读的习惯是大端序的,但是计算机先处理低位字节再处理高位字节时效率比较高,所以计算机更喜欢小端序。

640?wx_fmt=png

java中的编码

java中用来存储字符的类型有char和String,java规范中指出,char是由UTF-16编码格式的二字节码元来存储字符的。一个char占2个字节,即一个码元的大小,那么对于那些需要2个以上的字节存储的字符,是不能用char来保存的。String也是使用的UTF-16编码方式进行存储数据的,String可以用char[]数组进行存储,也可以用byte[]数组进行存储,这取决于字符串内字符的编码范围。

在Sun JDK6中有一个“压缩字符串”(-XX:+UseCompressedString)的功能。启用后,String内部存储字符串内容可能用byte[],也可能用char[]。当整个字符串所有字符都在ASCII编码范围内时,就使用byte[]来存储,此时字符串就处于“压缩”状态;反之,只要有任何一个字符超出了ASCII的编码范围,就退回到用char[]来存储。

下面我们来用一个简单的例子来看java中的字符编码,具体的代码如下:

private static String getHex(String str, Charset charset){	
    byte[] bytes = str.getBytes(charset);	
    StringBuilder sb = new StringBuilder();	
    for(int i=0,s=bytes.length;i<s;i++){	
        byte b = bytes[i];	
        sb.append(byte2Hex(b));	
        if(i<s-1){	
            sb.append(" ");	
        }	
    }	
    return sb.toString();	
}	
private static String byte2Hex(byte b){	
    // byte(8位)转int(32位)时,高24位会被自动补齐1,而byte原本高24位是0,	
    // 补齐之后二进制的补码值就变了,为了保持byte的值不变,与上0xff,	
    // 这样高24位变为0,低8位保持不变	
    String hexStr = Integer.toHexString(b & 0xff);	
    if(hexStr.length()==1){	
        hexStr = "0"+hexStr;	
    }	
    return hexStr;	
}	
private static void encode(){	
    // 编码的过程	
    String cn = "语";	
    String en = "A";	
    System.out.println(cn+"--encode with ASCII=======>"+getHex(cn,US_ASCII)); 	
    System.out.println(en+"--encode with ASCII=======>"+getHex(en,US_ASCII));	
    System.out.println(cn+"--encode with UTF-8=======>"+getHex(cn,UTF_8));	
    System.out.println(cn+"--encode with UTF-16======>"+getHex(cn,UTF_16)); 	
    System.out.println(cn+"--encode with UTF-32======>"+getHex(cn,UTF_32));	
}

上面的 encode 方法先执行 String.getBytes() 来获取字符串的字节数组,然后转成十六进制的结果输出。执行完将打印出下面的信息:

语--encode with ASCII=======>3f	
A--encode with ASCII=======>41	
语--encode with UTF-8=======>e8 af ad	
语--encode with UTF-16======>fe ff 8b ed	
语--encode with UTF-32======>00 00 8b ed

首先我们需要知道 String.getBytes() 方法是获取指定字符的 外码 的过程,说到 外码 ,就需要知道与他对应的内码内码 是char或String在内存中存储时采用的编码方式,而 外码 则是字符在文件中存储,网络中传输时采用的编码方式。

第一行打印出来的 3f ,表示字符 ”语“ 在ASCII码表中没有找到对应的码点,所以编码的结果是返回了一个 ?

第二行打印出来的 41 ,就是字符 ”A“ 在ASCII码表中的码点,转换成十六进制后的结果。

第三行打印了三个字节,这与汉字 ”语“ 在UTF-8下的编码方式相符。

第四行就比较奇怪了,按照UTF-16编码方式,”语“ 的编码结果应该是 8b4d ,开头多出来的两个字节是什么情况呢?

其实上面,我们已经了解到UTF-16编码方式会有字节序的问题,如果不指定字节序的话,UTF-16编码会在结果的字节流开头加上两个字节表示字节序: fe ff 表示大端序, ff fe 表示小端序。

第五行打印的也和预期相符。

如果我们指定UTF-16编码的字节序,那么输出的结果就不会再多出两个用来表示字节序的字节了,如下代码所示:

private static void encode(){	
    // 编码的过程	
    String cn = "语";	
    String en = "A";	
    System.out.println(cn+"--encode with UTF-16BE====>"+getHex(cn,UTF_16BE));	
    System.out.println(cn+"--encode with UTF-16LE====>"+getHex(cn,UTF_16LE));	
}

执行完之后,打印出如下的结果:

语--encode with UTF-16BE====>8b ed	
语--encode with UTF-16LE====>ed 8b

乱码

private static void decode(){	
    // 解码的过程	
    String cn = "语";	
    String en = "A";	
    byte[] cnUtf8Bytes = cn.getBytes(UTF_8);	
    byte[] cnUtf16Bytes = cn.getBytes(UTF_16);	
    System.out.println(cn+"--encode with UTF-8,decode with UTF-8========>"+new String(cnUtf8Bytes,UTF_8));	
    System.out.println(cn+"--encode with UTF-8,decode with UTF-16=======>"+new String(cnUtf8Bytes,UTF_16));	
    System.out.println(cn+"--encode with UTF-8,decode with UTF-16BE=====>"+new String(cnUtf8Bytes,UTF_16BE));	
    System.out.println(cn+"--encode with UTF-8,decode with UTF-16LE=====>"+new String(cnUtf8Bytes,UTF_16LE));	
    System.out.println(cn+"--encode with UTF-16,decode with UTF-8=======>"+new String(cnUtf16Bytes,UTF_8));	
    System.out.println(cn+"--encode with UTF-16,decode with UTF-16======>"+new String(cnUtf16Bytes,UTF_16));	
    System.out.println(cn+"--encode with UTF-16,decode with UTF-16BE====>"+new String(cnUtf16Bytes,UTF_16BE));	
    System.out.println(cn+"--encode with UTF-16,decode with UTF-16LE====>"+new String(cnUtf16Bytes,UTF_16LE));	
    byte[] enUtf8Bytes = en.getBytes(UTF_8);	
    byte[] enUtf16Bytes = en.getBytes(UTF_16);	
    System.out.println(en+"--encode with UTF-8,decode with UTF-8========>"+new String(enUtf8Bytes,UTF_8));	
    System.out.println(en+"--encode with UTF-8,decode with UTF-16=======>"+new String(enUtf8Bytes,UTF_16));	
    System.out.println(en+"--encode with UTF-8,decode with UTF-16BE=====>"+new String(enUtf8Bytes,UTF_16BE));	
    System.out.println(en+"--encode with UTF-8,decode with UTF-16LE=====>"+new String(enUtf8Bytes,UTF_16LE));	
    System.out.println(en+"--encode with UTF-16,decode with UTF-8=======>"+new String(enUtf16Bytes,UTF_8));	
    System.out.println(en+"--encode with UTF-16,decode with UTF-16======>"+new String(enUtf16Bytes,UTF_16));	
    System.out.println(en+"--encode with UTF-16,decode with UTF-16BE====>"+new String(enUtf16Bytes,UTF_16BE));	
    System.out.println(en+"--encode with UTF-16,decode with UTF-16LE====>"+new String(enUtf16Bytes,UTF_16LE));	
}

执行完,将打印出如下结果:

语--encode with UTF-8,decode with UTF-8========>语	
语--encode with UTF-8,decode with UTF-16=======>�	
语--encode with UTF-8,decode with UTF-16BE=====>�	
语--encode with UTF-8,decode with UTF-16LE=====>꿨�	
语--encode with UTF-16,decode with UTF-8=======>����	
语--encode with UTF-16,decode with UTF-16======>语	
语--encode with UTF-16,decode with UTF-16BE====>语	
语--encode with UTF-16,decode with UTF-16LE====>�	
A--encode with UTF-8,decode with UTF-8========>A	
A--encode with UTF-8,decode with UTF-16=======>�	
A--encode with UTF-8,decode with UTF-16BE=====>�	
A--encode with UTF-8,decode with UTF-16LE=====>�	
A--encode with UTF-16,decode with UTF-8=======>��A	
A--encode with UTF-16,decode with UTF-16======>A	
A--encode with UTF-16,decode with UTF-16BE====>A	
A--encode with UTF-16,decode with UTF-16LE====>�䄀

可以看到,用一种编码方式编码出来的结果,用另一种编码方式去解码,就会出现乱码的情况。甚至用相同的编码方式,解码时指定的字节序不同也会出现乱码的情况。

实用工具介绍

我们在处理自定义协议,或者抓包到一段报文时,常常需要进行协议的解析,而这时通常需要进行字符的解码。但是码流是用什么格式编码的我们是不知道的,为此笔者自己写了一个实用的工具,可以将一段字符编码成不同格式,也可以将一段码流用不同的编码方式进行解码。话不多说,直接看图:

Text2Hex

将字符用不同编码方式进行编码,并转成十六进制:

640?wx_fmt=png

Hex2Text

将十六进制的码流用不同的编码方式进行解码:

640?wx_fmt=png

Socket client

一个tcp客户端,连接上服务端后,可以发送数据,并将接收到的结果,转换成十六进制码流,然后自动用不同的编码方式进行解码,一眼就可以看出对方采用的何种编码方式:

640?wx_fmt=png

时间戳转换和md5计算

另外两个常用的工具是时间戳转换和md5计算

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=png

640?wx_fmt=jpeg

基于51单片机,实现对直流电机的调速、测速以及正反控制。项目包含完整的仿真文件、源程序、原理图和PCB设计文件,适合学习和实践51单片机在电机控制方面的应用。 功能特点 调速控制:通过按键调整PWM占空比,实现电机的速度调节。 测速功能:采用霍尔传感器非接触式测速,实时显示电机速。 正反控制:通过按键切换电机的正和反状态。 LCD显示:使用LCD1602液晶显示屏,显示当前的速和PWM占空比。 硬件组成 主控制器:STC89C51/52单片机(AT89S51/52、AT89C51/52通用)。 测速传感器:霍尔传感器,用于非接触式测速。 显示模块:LCD1602液晶显示屏,显示速和占空比。 电机驱动:采用双H桥电路,控制电机的正反和调速。 软件设计 编程语言:C语言。 开发环境:Keil uVision。 仿真工具:Proteus。 使用说明 液晶屏显示: 第一行显示电机速(单/分)。 第二行显示PWM占空比(0~100%)。 按键功能: 1键:加速键,短按占空比加1,长按连续加。 2键:减速键,短按占空比减1,长按连续减。 3键:反切换键,按下后电机反。 4键:正切换键,按下后电机正。 5键:开始暂停键,按一下开始,再按一下暂停。 注意事项 磁铁和霍尔元件的距离应保持在2mm左右,过近可能会在电机动时碰到霍尔元件,过远则可能导致霍尔元件无法检测到磁铁。 资源文件 仿真文件:Proteus仿真文件,用于模拟电机控制系统的运行。 源程序:Keil uVision项目文件,包含完整的C语言源代码。 原理图:电路设计原理图,详细展示了各模块的连接方式。 PCB设计:PCB布局文件,可用于实际电路板的制作。
【四旋翼无人机】具备螺旋桨倾斜机构的全驱动四旋翼无人机:建模控制研究(Matlab代码、Simulink仿真实现)内容概要:本文围绕具备螺旋桨倾斜机构的全驱动四旋翼无人机展开研究,重点进行了系统建模控制策略的设计仿真验证。通过引入螺旋桨倾斜机构,该无人机能够实现全向力矢量控制,从而具备更强的姿态调节能力和六自由度全驱动特性,克服传统四旋翼欠驱动限制。研究内容涵盖动力学建模、控制系统设计(如PID、MPC等)、Matlab/Simulink环境下的仿真验证,并可能涉及轨迹跟踪、抗干扰能力及稳定性分析,旨在提升无人机在复杂环境下的机动性控制精度。; 适合人群:具备一定控制理论基础和Matlab/Simulink仿真能力的研究生、科研人员及从事无人机系统开发的工程师,尤其适合研究先进无人机控制算法的技术人员。; 使用场景及目标:①深入理解全驱动四旋翼无人机的动力学建模方法;②掌握基于Matlab/Simulink的无人机控制系统设计仿真流程;③复现硕士论文级别的研究成果,为科研项目或学术论文提供技术支持参考。; 阅读建议:建议结合提供的Matlab代码Simulink模型进行实践操作,重点关注建模推导过程控制器参数调优,同时可扩展研究不同控制算法的性能对比,以深化对全驱动系统控制机制的理解。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值