• 计算机内部编码


    计算机按Byte表示线性地址,可以说,对于写程序来说,最底层的很少是二进制位,而是Byte。

    一般来说在汇编里用到三种长度的数据结构:Byte、Word和DWord(Cardinal)。

    计算机在存取后两者时,Intel的习惯是Little Endian,即Low Word或Low Byte在前面,与我们的日常习惯相反。

    GB2312-80标准包括6763个汉字,分94个区,每个区94个位,所以一种叫区位码的东西可以用来准确定位一个汉字。

    用1Byte里的7Bit表示一个ASCII字符,00H~7FH,共2^7=128个。

    用1Word表示GB码汉字,但为了与ASCII字符区分开,其中的两个Byte都要大于A1H;即若内存中有1Byte范围处于A1H~FFH,则应为一个GB汉字的一半。大部分固定长度的编码都是用1Word来编码,这样做缺点很明显,容易浪费空间。在各种语言中的宽字符(比如C++的WCHAR,也叫wchar_t)就常用于处理固定长度编码的字符,通常这些宽字符都是用1Word大小的整型数据类型通过Macro来定义的。GB2312不算是固定长度的编码,毕竟它兼容ASCII码,对这些字符仍用1Byte来编码。要让我举个固定长度编码的编码方案还真是有点难为我,想想还是UTF-16的大多数情形比较“固定长度”。

    Unicode,是个组织,也是个为了国际化提出的编码方案,不过这么说并不恰当,他与ASCII或GB2312在性质上存在区别。它只提供了码点(Code Point,通常的书写格式是“U+四个或更多个Hex数字”)与我们最终要看到的字形(glyph)的映射关系,通过这种映射关系每个字形都可以用一个或几个码点表示出来;但具体用什么二进制表示什么码点,还需要UTF(Unicode transformation format)。UTF规定了最终的底层编码,将码点映射到了Byte序列,常见的UTF就是UTF-8了,当然还有UTF-16和UTF-32等等。相比较而言,对于英语语系国家的人而言,UTF-8编码比较节省存储空间。下面会提到这一点。

    还有个与Unicode组织无论是时期上还是性质上都类似的ISO10646,是ISO组织的一个项目。两个组织都致力于创造一个国际化的超级字符集,并在九十年代初开始合作。但无论如何,ISO10646提出的编码方案叫做UCS,与Unicode大同小异,有很大的兼容性。在ISO10646这里,UTF是UCS transformation format的缩写。
    UTF是不固定长度的编码方式。不过无论哪种UTF,最长的情况下,一个码点也不会用超过1Cardinal的长度来编码。

    UTF-8的编码原则简单点可以这样表述:
    我们知道一个Byte的范围是00H~FFH,可以分为00H~7FH,80H~BFH,C0H~FDH和FEH与FFH
    ASCII字符处于00H~7FH,UTF-8里就用一个Byte来记录ASCII字符的码点(二进制的特点就是以一个0开头的8bit),这样就向下兼容了ASCII字符集。
    所有其他的字符都要用多于一个Byte来表示,即,多字节字符。
    多字节字符的第一个字节处于C0H~FDH(从11100000B到11111101B,分为110xxxxxB,1110xxxxB,11110xxxB,111110xxB和1111110xB五个子集),记录了该多字节字符占用了多少字节(就是要看这8bit的前几位,在第一个0前面1的个数就是占用了多少个字节;这也是上面把这个区间分成五个子集的原因,每个子集都属于不同长度的Byte流,而上面的x则记录了真正的编码信息)。接下了的几个字节都处于80H~BFH(在编程时只要看这8bit的前两位是不是10就可以了),记录了编码用的信息(10后面的6bit记录了真正的编码信息)。之所以设计为这个范围,是为了让UTF-8更健壮——如果程序得到的UTF-8编码字符串是残破的,这样的设计不会导致全部乱码;只要一个ASCII字符或多字节字符的信息是完整的,那么它就能被正确解码(实际操作上就是看这8bit的开头几位来判断这个Byte是属于哪种类型,是否符合0xxxxxxx或者一个头Byte和相应几个10xxxxxx的规律)。最后的两个FEH与FFH在UTF-8中不会出现。
    在编码时,我们的input是一个码点U+XXXXXX,output是一个Byte流。首先看input该用多少个Byte才能表示出来(一般是按占用Byte数预先分出几个区间,再看该码点位于哪个区间),然后就创建这几个Byte,比如是3个Byte时是1110xxxxB,10xxxxxxB,10xxxxxxB,然后把码点信息按顺序记录到x中,如果x多出了几位,就在多出的x上补0,注意,是从高位补0。

    诸如UTF-16的编码方式,ASCII码也用了两个Byte(第一个是0)来表示,所以上文说,UTF-8对于英语语系的人来讲比较省空间。而对于我们中国人,情况则相反,UTF-8使用3个Byte才能表示一个汉字,而UTF-16则只使用2个Byte,与GB2312相同的空间消耗。但是UTF-8码貌似在设计上就比UTF-16有更大的编码容量,毕竟它是改进后的一种编码。

    在Ruby中,String类就是Byte的序列,也就是说,一个Ruby字符串跟C语言里的char[]的很类似,就是一串Byte,用不同编码方式来解码得到的结果会不同;当然,除了两种编码方式存在兼容的情况,一般情况下只有一种编码方式是正确的,其余的就解码出了乱码。当你在你的Ruby程序源代码里给一个字符串赋值时,这个字符串最终变成的Byte序列是什么取决于你的源代码.rb的编码方式;想想看,ruby.exe要做的无非就是一个Byte一个Byte地读取.rb文件的内容,遇到字符串赋值之类的语句时把该字符串的Byte序列(比如两个双引号间的n个Byte)存储到对应的字符串变量中,而你在使用该字符串变量时自然就应按照该.rb文件的编码格式来解码。

    举我自己的例子,首先源代码里有一句str="北关大街",如果使用XP系统,而XP的Locale是cp936,也就是简体中文的环境,系统默认的编码方式是GB2312,用记事本编辑好源程序文件(假设是test.rb)后就使用默认的GB2312来编码,Ruby在执行时就会让str="\261\261\271\330\264\363\275\326",长度是8(因为在test.rb里“北关大街”就是按照GB2312编码的Byte序列,ruby.exe只是直接copy到str里了);如果使用RedFlag Linux,因为这个系统的locale设置为UTF-8,用kwrite写好test.rb后就默认使用了UTF-8来编码,最终str的Byte序列就是用UTF-8编码的了(同上,把Byte流直接copy到str的结果),因为UTF-8编码表示一个汉字需要三个Byte,所以最终str的长度是12。当然,各种编辑器,只要是稍微高档点的,都可以让你选择编码方式,而不是只取决于系统环境。

    说到底,ruby.exe程序可以直接解释执行以ASCII(或兼容ASCII的其他编码方式)编码的源程序,这就是对.rb文件在编码上的唯一要求。KCode开关目前只对日本人有用,他提供了对日本的两个字符集的支持;很有可能这两个字符集对ASCII不是向下兼容的,否则就没有必要提供这个KCode开关的功能。比如我们这里安装的最多的简体中文版的XP的默认编码方式是GB2312,它兼容了ASCII字符集,我们就可以用ruby.exe直接解释执行使用GB2312编码的源程序。不过前面提到了UTF-8也是兼容了ASCII的,可当我们用记事本写了一个ruby程序并用UTF-8编码保存后,ruby.exe竟无法解释执行!为什么呢?这跟“记事本”有关,不信的话,你就用vim或者kwrite来写一个相同的程序并用UTF-8保存下看看,绝对是可以执行的,哪怕源代码里有汉字(当然,同其他情况相同,汉字仅限于注释和字符串的内容)。其实“记事本”为了在加载一个文件时可以快速判断出该文件是否使用UTF-8编码,就在它编辑过的UTF-8编码的文件头部加了2个Byte的标记信息,我没记错的话应该是一个$FF和一个$FE。刚才ruby.exe在加载脚本时在文件一开头就碰上了这两个非法的字符,就认为该文件无法解释执行。

    既然讲到这儿,注意不要把ruby.exe的KCode开关与Ruby语言的jcode库的$KCODE混淆。很明显,KCode开关是让ruby.exe知道怎么去读取test.rb,如果ruby.exe连test.rb的内容都读成了乱码,还怎么去理解test.rb里的$KCODE=u和require 'jcode'?这个$KCODE的功能在于,一经require 'jcode',你的String类马上多出了几个从jcode里mixin来的方法,这几个方法通过$KCODE的设定来操作字符串。提到的mixin是Ruby的特性,使用模块的术语,而Ruby的模块类似于Delphi的接口和Java的接口。

    上文的str="\261\261\271\330\264\363\275\326"是不是让你晕了一小下?在Ruby中使用转义字符结合八进制数字的双引号字符串可用来表示字符串。这种方法直接指定了该字符串中的每个Byte的值。Ruby默认的是八进制(比如puts字符串时对于无法直接显示的字符就用转义字符结合八进制显示),当然也可以用转义字符结合十六进制;不过在转义字符这里不是按照Ruby通常的表示各种进制的数的习惯,转义字符后的数不管开头是不是0都按八进制处理,但如果这个数里有大于等于8的数字时就按十进制处理,如果是以x开头,则按十六进制处理。注意这里的每个三位的八进制数都不会占用超过1Byte的空间(即不允许该八进制数超过0377,也就是0xFF),如果你用这种方法给字符串赋值时让其中一个三位八进制数超过了八进制数377,得到的则必然是个错误的结果。

    与Ruby的String类相对的就是Java的String类了。Java的String类不是Byte流,是char流,注意这里的char不是C\C++\Pascal里的char,而是Java里的数据类型char,它占用了两个Byte,而不是那些传统语言的一个。我们知道,Java语言的char类型的范围是Unicode的字符集,而具体来说,每个Unicode字符通过UTF-16的方式编码到了一个char(也就是两个Byte)里。而似乎让我自相矛盾的是上文提到UTF都是不固定长度的编码,那么UTF-16未必就保证能让一个Unicode字符存储在一个char里吧?的确,大部分的Unicode字符使用UTF-16编码都是一个char,比如中文,但有些字符这一个char是不够的,这时就要用两个char来编码(上文提到过,UTF最长不会超过1Cardinal=2Java_char=4Byte)。也正是如此,严格来讲,Java里的char是低级的,不提倡使用的,因为它没能达到Java所期望达到的一个字符型变量可以存储任何一个字符的期望,而之所以char之所以还是2个Byte,还存在,则是因为Java历史也算悠久,它是同编码方式一同发展着的原因。建立在char流基础上的String类,它的自身必然的要以UTF-16编码,也就是你不能尝试着像在其他语言里那样用其它的编码方式解码Java的String,你也解不了。这种机制使Java语言在国际化、跨平台上有很大的便捷性。

    然而有的时候,你的程序本身就是搞文字编码的,怎么办?Java的String类提供了getBytes()及几个overdrive方法,可以安照特定编码方式(首先这台机器上得安装了这种编码方式,否则抛出一个异常)把一个String转化成一个Byte流。比如byte[] bytes=str.getBytes("UTF-8"),这样bytes里就存储了以UTF-16编码的字符串str映射到UTF-8编码方式的Byte流。如果str还是"北关大街"的话,str.length是8,bytes的长度最后就是12。这个方法要放在try catch块里才能使用,以便抛出异常处理异常。如果不指定该方法的参数,比如str.getBytes()则返回按照当前系统默认编码方式编码的Byte流,我的XP下就是GB2312。当然,有转成Byte流的,就有从Byte流转回去的,那就String类的几个overdrive的构造方法。比如bytes是按照GB2312编码的Byte流,要转化成一个字符串,需要String new_str=new String(bytes,"GB2312"),这样一个以UTF-16编码的字符串new_str就诞生了。

    最后讲一种很有用的编码,Base64。它可以把二进制数据编码成可打印字符的字符串列表。我经常在查资料的时候把网页顺手保存成mht格式(兼容性很烂的格式,不过就是因为只生成一个文件我才经常用),网页里的图片什么的资源都是用Base64编码后以文本的形式保存的。Ruby对Base64提供了不错的支持,Array类的pack以及String类的unpack都支持该编码,并且还可以通过加载Base64库来获得更多的Base64支持。在不方便使用二进制的时候试下Base64吧,通过irb打几行语句就可以搞定。

  • 相关阅读:
    小程序支付
    小程序传参
    git
    学习过程遇到问题的解决方法
    进程创建注意事项:函数及对应包
    进程创建:函数及对应包
    mac解决eclipse 端口占用问题
    暴力
    doc2vec
    Java正则表达式的解释说明
  • 原文地址:https://www.cnblogs.com/BeyondTechnology/p/1963325.html
Copyright © 2020-2023  润新知