现在的位置: 首页 > 综合 > 正文

VB 与 UniCode(合篇)

2013年08月03日 ⁄ 综合 ⁄ 共 14239字 ⁄ 字号 评论关闭
VB 与 UniCode(合篇)
点睛工作室--梁利锋

声明

个人可以自由转载本文,不过应保持原文的完整性,并通知我;商业转载先请和我联系。

本文没有任何明确或不明确地提示说本文完全正确,阅读和使用本文的内容是您自己的选择,本人不负任何责任。

如果您发现本文有错漏的地方,请您给我指出;如果有什么不理解的,请您给我提出。

意见、建议和提出的问题最好写在我的主页 http://llf.126.com 的留言版上。

(阿涛按:本文由梁利锋兄的两篇文章整合而成,本文保留了原文的所有内容和叙述顺序)

第一篇:VB 与 UniCode (2000.03.27)

一、UniCode

UniCode 作为一个名词应该是非常著名的了,特别是在我们这样一个非英语国家里。UniCode 是一种联合的符号内码,因为用两个字节表示,所以可以表示世界上的大多数语言。我在某些书上见到说 UniCode 分两种编码转换格式,UTF-8 和 UTF-16 ,其中的 UTF-8 编码是变长的,长度从一个字节到三个字节不等,它的优势在于英文不用处理就符合此编码,UTF-16 编码和 UniCode 的编码基本相同,可能在文本转换上有应用吧?有兴趣的朋友可以通过 E-mail 或者留言版和我讨论此编码 。本文不讨论这些编码转换格式,而主要讨论UniCode的内存处理,长度固定两个字节,因为这种编码是 Windows 系统内定的 Unicode 编码方式,另外长度固定对于字串的处理也非常有利,个人认为用处更大一些。

我不想在概念上做太多的文章,大家只要记住 UniCode 是两个字节就可以了。作为英文,在 UniCode 里的编码是首字节置零,尾字节就是原来的英文码,比如“A”的十六进制码是“41”,而其 UniCode 的十六进制码是“0041”。不过千万不要认为 GB 码的汉字编码就和 UniCode 相同,其实几乎都不相同,所以一般要一个对照表进行转换,不过各语言版本的 Windows 自带不同的编码和 UniCode 之间互相转换的函数,我们就不需要关心细节了。Windows的可执行文件中经常使用 UniCode 作为存储格式,使用十六进制的编辑器就可以看到,如果是英文,就显示成每个字母前有一个“00”,但是如果是中文而又存成 UniCode的话,是不能察看到的,以前我曾经说过变量使用中文没有问题,因为在可执行文件里查不到那些中文,但是如果是UniCode的话,查不到也不表明没有,所以我又把文件转换成 ANSI格式,再次查找,仍然没有,也就是说结论没有变化,我们还是可以使用中文变量名的。

因为 Windows 的可执行文件编译有使用 UniCode 的习惯,所以海峡对岸的同志们编写的软件虽然没有专门制作 GB 版,但是像菜单标题之类的东西显示也是正常的,不过既然只是“经常”,而不是“总是”,所以很多对话框的显示却是乱码,也就可解释了。进一步推想,如果所有的字符串都存储为 UniCode的话,我们就不需要等待同一个软件的 GB 版和 BIG5 版了,当然,我们看到繁体字习以为常,但是他们看到简体字时,不知道会是什么感想?

另外,像 GB、Big5 之类的编码,都是考虑到和英文的兼容性的,比如 GB 码,就是将首位置“1”,当然,现在的 GBK 内码只是第一个字节首位置“1”,第二个字节作了扩展,也可以是 ASCII 值小于 127 的值了。在简体中文版 Windows 95 的目录下有一个“GBK.TXT”文件,罗列了 GBK 内码所有的字,有兴趣可以看一看。顺便说一下,以前常用的 GB2312 内码只有六千多字,Dos下的汉字系统都是使用此内码的,所以也只能显示这六千多字,而 GBK 内码有两万九千多字,内含繁体编码,不过字体文件不一定支持 GBK ,我所知的微软的 GBK 字体包括 Windows 自带的“宋体”、“黑体”,Office 自带的“隶书”、“幼园”等,其它的字体文件一般都是 GB2312 的,包括 Windows 自带的“楷体_GB2312”、“仿宋_GB2312”,还有其它的像“微软简行楷”、“微软简魏碑”等,其它公司制作的字体文件我至今没有见过支持 GBK 的,实在很遗憾,而且微软好像也没有再制作 GBK 字体,大概是只顾得打官司了吧?!

二、VB 和 UniCode 的关系

在 C 语言中,内部的字符串是 ANSI 格式,也就是以字节为单位,但是在 VB 中字符串是 UniCode 格式,也就是说以字为单位,为了和类 C 语言相区别,我把以 UniCode 表示的字符串称之为字串。

既然 VB 中字串是 UniCode 格式,我们就知道 Len("ABC测试") 等于 5 , LenB("ABC测试") 等于10,当然,因为 Win9x 系统内部并不使用 UniCode ,所以在和 API 接口时就会出现问题了。我们经常可以见到 API 的声明函数最后由一个“A”,比如“SetWindowTextA”“GetPrivateProfileStringA”等等,这是表明此函数使用 ANSI 字符串格式;相对的,也有使用 UniCode格式字串的相同功能的函数,后缀为“U”,比如“SetWindowTextU”“GetPrivateProfileStringU”等等,不过这些 UniCode 格式的函数一般仅用于 Windows NT ,Windows 9x 上很少有,另外,因为 Windows NT 也支持 ANSI 格式的函数,所以平时我们调用的仍然是 ANSI格式的函数,也所以 VB 在调用 API 时,都会把字串转换成字符串,以便和 ANSI 函数相兼容。

不过 Windows 9x 中也并不是没有 UniCode 函数,比如 OLE 自动化函数就全部是 UniCode 函数,如上,VB会把自己的 UniCode 字串转换成 ANSI 字符串,所以调用这些 OLE 自动化函数一般不能用“Declare”语句定义,当然,事实上 VB 和 OLE自动化关系十分密切,VB中的很多功能都是建立在 OLE 自动化的基础上的(比如读取 JPG、GIF 等图形文件就是使用的 OLE 自动化函数),所以 VB 在内部调用 OLE 自动化函数,这样就不需要作转换了,也因此,VB内部调用 OLE 自动化函数的速度比 VC 调用 OLE 自动化函数的速度要快,因为VC要先做 ANSI 字符串到 UniCode 的转换,不过这种优势并不太明显,就像 VB调用 ANSI 格式函数时速度比 VC 慢的劣势也不明显一样。(需要注意,VB 使用和 OLE 自动化同样的变体类型的变量,这也是 VB 调用 OLE 自动化函数速度快的一个原因,使用其它语言调用 OLE 自动化函数可能需要自己做普通变量到变体变量的转换)

因为 VB 内部使用 UniCode,很多人在使用 VB 编程处理字串的时候都或多或少地遇到了问题,因此很多人在介绍在 VB 中处理字串的技巧的时候总会带出一些对 VB 的不满,因为不能用老的对字符串的认识来处理字串了,不过我认为这种认识是不对的,正是因为我们是在一个非英语国家,VB 的这一特性才更有用处,当然,如果能多了解关于 UniCode 的知识会更有用。

在VB中还有一种处理字串的特殊方法,用来解决我们遇到需要对字串按字节方式访问的问题,看一下以下的代码,大家应该可以明白:


Option Explicit

Private Sub Form_Load()
    Dim 字节数组() As Byte
    Dim 字串 As String
    字串 = "测试"
    字节数组 = 字串 & "成功"
    字串 = 字节数组
    MsgBox 字节数组, vbOKOnly, 字串
End Sub


我们看到,在以上程序中,字节数组可以和字串互相赋值,而且字节数组在取得字串时会自动调整大小以适应字串,这一功能非常有用,而且非常方便,不过因为 VB 内部是 UniCode的,所以我们处理时必须以两个字节为单位,否则会造成处理错误,当然,因为总是以两个字节为单位,所以也是很方便的,比起老的字符串方式处理中文可是方便的太多了。VB 中的 Integer 是十六位的,也就是两个 Byte ,那么使用 Integer 数组处理字串岂不是更好?好的,VB 支持 Integer 数组和字串的互相赋值吗?不支持!

三、VB 中快速处理 UniCode

VB 中处理 UniCode 是有一些问题需要解决的。

首先,我们遇到的问题是如何取得中英文混合字串在 ANSI 格式时的长度?VB当然是可以测量其长度的,不过需要用到字符串转换函数:LenB(StrConv("ABC测试",vbFromUnicode)) 。好的我们来看看,首先我们把字串转换成字符串 StrConv("ABC测试",vbFromUnicode),然后用 LenB 函数取得字符串的长度,显然,在这里我们做了一些的无用功——字串转换,这是非常费时间的,但是因为 VB 内部是使用 UniCode 格式的,所以倒也不可避免,不过在某些情况下可以将这一次转换和其它的转换合并成一次以减少时间消耗。

在了解转换合并之前,我们先来了解一下 VB 调用 API 时的字串参数的处理方法。来看一个简单的 API 函数:


Declare Function SetWindowText Lib "user32" Alias "SetWindowTextA" _
    (ByVal hwnd As Long, ByVal lpString As String) As Long


在这个函数中有两个参数,一个是窗口句柄 hwnd,一个是要设置的窗口标题指针 lpString 。我们知道在 VB 里使用 ByVal 关键字传递字串参数,当然,在其中并不是真的按值传递,VB 在这时作了一次 UniCode 到 ANSI 的转换,然后把转换后的 ANSI 字符串的指针传递给了 API 函数,因为有时候 API 不止利用字符串指针传入字符串,也会传出字符串,所以 VB 在 API 函数调用结束时又作了一次 ANSI 到 UniCode 的转换,把转换的结果传回到原来的字串里,在这个函数里是 lpString 。假设在你的程序里要调用 SetWindowText ,而且又要得到字符串的长度,那么程序一般是这样的:


Public Function 设置标题(标题 As String) As Long
    设置标题 = LenB(StrConv(标题, vbFromUnicode))
    Call SetWindowText(Me.hWnd, 标题)
End Function


程序可以说是非常简单,不过这里所作的额外操作还是很多的,如前所述,调用 SetWindowText 时会有一次 UniCode 到 ANSI 的转换,还有一次 ANSI 到 UniCode 的转换,另外,为了取得字符串的长度,程序本身还做了一次 UniCode 到 ANSI 的转换,而且这些 VB 在内部进行的转换都另外申请了内存以便进行操作,其实这都是可以避免的,不过需要修改一下 API 的定义,另外需要用到字符串和 Byte数组之间能互相赋值的特性,而且还要用到一个未公开的函数 VarPtr 用来取得变量的地址:


Declare Function SetWindowText Lib "user32" Alias "SetWindowTextA" _
    (ByVal hwnd As Long, ByVal lpString As Long) As Long

Public Function 设置标题(标题 As String) As Long
    Dim 标题字符串() As Byte
    标题字符串 = StrConv(标题, vbFromUnicode)
    设置标题 = UBound(标题字符串) + 1
    Call SetWindowText(Me.hWnd, VarPtr(设置标题(0)))
End Function


这样,我们在调用此 API 函数时一共只分配了一次内存,作了一次 UniCode 到 ANSI 的转换就完成了和上一个函数同样的任务,速度大为提高,内存的需求量也有所降低。

当然,标题不可能很长,所以这个函数所得到的速度提升和性能改善并不能在程序中体现出来,而且 SetWindowText这个函数还会刷新窗口标题,会用到极其缓慢的 GDI 函数,怕是即使调用很多次,速度提升也会淹没在缓慢的 GDI 海洋里,在这里也只是作为一个例子来说明此种处理的优势,取该函数只是为了容易理解罢了,为了测试用这种方法究竟能得到多少的优势,应该选一个只作简单内存处理的 API (最好是自己做一个 DLL ,写一个什么也不做的函数)调用,不过我没有测试过,如果各位谁作了这种测试,不要忘了把结果告诉我:)

重新回到字串和数组的问题上。我们知道,VB 中处理字符串有很多函数,比如 MidLeftRight等,如果我们使用这些函数来取得字串的子串,会经过一次函数调用,一些对输入值的判断和转换,重新分配内存,返回。而这只是我们察看时的代价,如果要修改一个字串中间的一个字,我们就不得不取得此字前的字串,加上修改后的字,再加上此字后的字串,浪费就更大了。在这种时候,数组就能显出其优势了,因为对数组的操作事实上是指针操作,速度非常快,而且如果要修改其中的字,因为是指针操作,所以不需要提取字串的操作,只要简单的对要改的字操作就可以了:


Option Explicit

Private Sub Command1_Click()
    字串方式 "不好吗?"
End Sub

Private Sub Command2_Click()
    数组方式 "不好吗?"
End Sub

Private Sub 字串方式(字串 As String)
    Dim i As Integer, 新字串 As String, 字 As String
    For i = 1 To Len(字串)
        字 = Mid(字串, i, 1)
        If 字 = "好" Then
            新字串 = 新字串 & "坏"
        Else
            新字串 = 新字串 & 字
        End If
    Next
    MsgBox 新字串
End Sub

Private Sub 数组方式(字串 As String)
    Dim i As Integer, 新字串() As Byte, 字() As Byte
    字 = "好"
    新字串 = 字串
    For i = LBound(新字串) To UBound(新字串) Step 2
        If 新字串(i) = 字(0) And 新字串(i + 1) = 字(1) Then
            字 = "坏"
            新字串(i) = 字(0)
            新字串(i + 1) = 字(1)
            'Exit For '只替换第一个字的时候加上这一句
        End If
    Next
    MsgBox 新字串
End Sub


在实际的编程中间大概不会有第一个函数那样的操作方法,因为可以用 InStr函数得到某字的位置,但取出前后字串的操作还是需要的,所以仍然会比第二个函数的速度慢,当然,这也是在大量数据操作的时候,如果数据量较小的话,还是使用 Mid等函数的好,毕竟简单才是 Basic 的精华!

不过我们也见到了,在第二个函数中处理字的时候仍然很麻烦,因为要判断两次才能确定一个字,很不方便,仍然是上面的老问题:VB支持 Integer 数组和字串的互相赋值吗?答案当然也仍然是:不支持!不过我们可以自己设计这样的函数来实现这样的功能。

为了实现这一功能,首先我们要得到字串的地址,不过我试了很多办法,就是没有办法直接得到它的地址,很不幸的仍然需要使用Byte数组做桥梁,下面是我写的两个函数,一个实现从字符数组到字数组的转换,一个实现从字数组到字符数组的转换:


Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
	(lpvDest As Any, lpvSource As Any, ByVal cbCopy As Long)

Public Sub 字符数组到字数组(字符数组() As Byte, 字数组() As Integer)
    Dim 长度 As Long
    长度 = UBound(字符数组) - LBound(字符数组) + 1
    If 长度 >= 2 Then
        Redim 字数组(1 To 长度 / 2)
        CopyMemory 字数组(1), 字符数组(0), 长度
    End If
End Sub

Public Sub 字数组到字符数组(字数组() As Integer, 字符数组() As Byte)
    Dim 字长度 As Long, 字符长度 As Long
    字长度 = UBound(字数组) - LBound(字数组) + 1
    字符长度 = 字长度 * 2
    If 字长度 >= 1 Then
        If UBound(字符数组) - LBound(字符数组) + 1 <> 字符长度 Then
            Redim 字符数组(1 To 字符长度)
        End If
        CopyMemory 字符数组(0), 字数组(1), 字符长度
    End If
End Sub


所以用过程(Sub)也是迫不得已,只有这样我才能确定 VB使用的是快速的指针方式,而不是又创建了一个副本。现在,我们就可以使用以上的两个函数改写刚才的程序:


Option Explicit

Private Sub Command1_Click()
    数组方式 "不好吗?"
End Sub

Private Sub 数组方式(字串 As String)
    Dim i As Integer, 新字串() As Byte, 字() As Byte
    Dim 中间字串() As Integer, 中间字() As Integer
    字 = "好坏"
    新字串 = 字串
    字符数组到字数组 新字串, 中间字串
    字符数组到字数组 字, 中间字
    
    For i = LBound(中间字串) To UBound(中间字串)
        If 中间字串(i) = 中间字(1) Then
            中间字串(i) = 中间字(2)
            Exit For
        End If
    Next
    '也可以不用以下的一句,因为大多数的 API 需要的只是地址
    '所以如果和 API 接口的话, 需要做的是把 UniCode 转换成
    'ANSI,然后取得地址传递过去,如果和 UniCode 函数接口的
    '话,更可以直接把地址传过去。把 UniCode 转换成 ANSI 的
    '话,需要使用 API 函数 WideCharToMultiByte 。
    字数组到字符数组 中间字串, 新字串
    MsgBox 新字串
End Sub


再重申一次,这样做的原因是有大量数据需要处理,如果没有大量数据需要处理的话,一来不需要这么麻烦,二来可能速度还会减慢,大家可以分析一下,应该是可以明白的。那么什么时候是有大量数据,而且需要有这种方便的 Integer数组的呢?我遇到的是我做的内码转换器,使用这种方法的速度提升占总提升的 1/3 (另外的 2/3 在文件的读和写上,参见我写的《文件处理速度》),以下给出其中使用此方法的部分核心代码以供参考:


Private Function 字数组方式转换(字数组() As Integer) As Boolean
    Dim i As Long, 不使用多线程 As Boolean, n As Integer
    不使用多线程 = Not 使用多线程
    If 不使用多线程 Then DoEvents
    For i = LBound(字数组) To UBound(字数组)
        字数组(i) = 内码对照表(字数组(i))
        If 不使用多线程 Then
            n = n + 1
            If n > 1000 Then
                n = 0
                DoEvents
            End If
        End If
        If 中止 Then
            中止 = False
            字数组方式转换 = True
            Exit For
        End If
    Next
End Function


可以看到,这一段代码里也演示了在不使用多线程的时候怎么加快程序的执行速度,不过和本题无关,就不说了。

上面的注释中说道把 Integer 数组做 UniCode 到 ANSI 的转换需要使用 API 函数,这是因为 StrConv 也不认识 Integer 数组,下面把我写的关于文本文件操作的两个函数附上,其中利用了 API 函数 MultiByteToWideChar 和 WideCharToMultiByte,不过我们使用的是 Bruce McKinney 提供的类型库的函数说明:


Public Function 文本文件读入(文件名 As String, 内容() As Integer) As Boolean
    Dim 文件句柄 As Long, 文件长度 As Long, 总长度 As Long, 临时() As Byte
    文件名 = Trim$(文件名)
    文件句柄 = CreateFile(文件名, GENERIC_READ, FILE_SHARE_READ, _
        0&, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0&)
    If 文件句柄 <> 0 Then
        文件长度 = GetFileSize(文件句柄, 0&)
        Redim 临时(1 To 文件长度)
        If ReadFile(文件句柄, 临时(1), 文件长度, 文件长度, ByVal 0&) <> 0 Then
            Redim 内容(1 To 文件长度)
            总长度 = MultiByteToWideCharPtrs(CP_OEMCP, 0&, VarPtr(临时(1)), _
                文件长度, VarPtr(内容(1)), 文件长度 * 2)
            Redim Preserve 内容(1 To 总长度)
            文本文件读入 = True
        End If
        CloseHandle 文件句柄
    End If
End Function

Public Function 文本文件写入(文件名 As String, 内容() As Integer) As Boolean
    Dim 文件句柄 As Long, 文件长度 As Long, 总长度 As Long, 临时() As Byte
    文件名 = Trim$(文件名)
    文件句柄 = CreateFile(文件名, GENERIC_WRITE, 0&, 0&, _
        CREATE_NEW, FILE_FLAG_SEQUENTIAL_SCAN, 0&)
    If 文件句柄 <> 0 Then
        总长度 = UBound(内容) - LBound(内容) + 1
        Redim 临时(1 To 总长度 * 2)
        文件长度 = WideCharToMultiBytePtrs(CP_OEMCP, 0&, VarPtr(内容(1)), _
            总长度, VarPtr(临时(1)), 总长度 * 2, 0&, 0&)
        Redim Preserve 临时(1 To 文件长度)
        If WriteFile(文件句柄, 临时(1), 文件长度, 文件长度, ByVal 0&) <> 0 Then
            文本文件写入 = True
        End If
        CloseHandle 文件句柄
    End If
End Function


上面的函数中各位只需注意 UniCode 和 ANSI 互相转换的函数就可以了,其中的“CP_OEMCP”常数指的是当前版本的 Windows 使用的缺省代码页,比如在简体中文版 Windows上调用 MultiByteToWideCharPtrs 函数时使用 CP_OEMCP 常数,就会把 GBK 内码的文字流转换成 UniCode 内码的文字流,而如果在繁体中文版的 Windows 上,同样的程序就是把 BIG5 内码的文字流转换成 UniCode 内码的文字流;相对的,WideCharToMultiBytePtrs 函数使用 CP_OEMCP 常数就是把 UniCode 文字流转换成当前语言内码的文字流。至于使用 API 函数进行读写操作,正像 Bruce McKinney 所说,是因为和其它 API 函数的兼容性考虑,不过不再说了,不然就离题太远了。

结语:UniCode 随想

回想一下我们处理 UniCode 的方法。首先,我们把文本文件读出,然后把读出的内容使用 API 函数转换成 UniCode ,在 UniCode 的世界中,我们处理各种语言都会得心应手,但是 Windows 的字库是各种方言的字库,比如 GBK 、BIG5 等,如果我们可以使用 UniCode 字库的话,那么一套字库就可以支持所有国家版本的 Windows,而不会造成每安装一种语言支持就需要安装一套新的字库,这样显示的时候调用的就只是一套字库,操作系统需要提供的只是各国编码和 UniCode 的转换函数罢了。

我们知道 WindowsNT 是从底层开始重新设计的操作系统,所以 NT 其实也是从底层支持 UniCode 的,现在 Windows 2000 使用和 NT 同样的构造,就是说它也是从底层开始支持 UniCode 的,事实上 Windows2000 会出一种所谓的世界版,用户可以选择添加对多种语言的支持,并且选择多种语言的字库,在 Internet 高度发达的今天,这种方便性当然是无可置疑的,我坚决拥护。不过我的推想是如果世界上存储资料的时候都使用 UniCode,而不是各自的编码方法,则对于我们在各国之间传输资料将会有非常大的帮助,如果真的完全实现了这一点,操作系统连提供各国编码和 UniCode 的转换函数都将成为多余的了!

HTML 现在是网络上流行的的格式,不过 XML 的发展应该会替代今天 HTML 的地位,而 XML 的标准中有一项规定:必须支持 UniCode(同时也可以选择性的支持 GBK、BIG5 等编码)。UniCode 是英语国家制定的标准,XML 也是英语国家制定的标准,这让人有些遗憾 —— 非英语国家为了使用计算机,想出了各种方法来让计算机支持本语种,却没有想到兼容其它国家(除了英语)的问题,最后仍然是英语国家找到了解决方案。而在已经有了解决方案的时候,我们却还懵然不觉,甚至有些不愿意了解,才是更可遗憾的地方。

有集体观的个人才有发展,有国家观的集体才有发展,有世界观的国家才有发展,不是吗?


第二篇:VB 与 UniCode 补遗 (2000.05.24)

另外的问题

和伟乾通信,说是对于《VB 和 UniCode》看不太懂,于是我也再看了一遍,确实有很多地方显得晦涩了一些,特别是关于 UniCode 的部分说的很不清楚,而且太专注于怎样快速处理 UniCode,对于普通的处理方法却没有介绍清楚,这一次是要做一些打补丁的工作,希望尽量专注于 UniCode的特性的介绍,阿门。:) 不过因为 Windows 并不是使用 VB 开发的,而且 VB 也不是使用 VB 开发的,所以大多数的例子使用 C 语言格式,希望大家能看得懂,阿门 again :)

顺便说一下,上一次的《VB 与 UniCode》的第三小节有一个“StrConv ("ABC测试",vbFromUnicode,vbFromUnicode)”是错的,应该是“StrConv("ABC测试",vbFromUnicode)”,现已改正。

其实GBK 、BIG5之类的编码被称为双字节编码,而 UniCode被称为宽字节编码,因为宽字节编码在程序设计上更方便,所以比较流行。

另外需要注意,在本篇文章中所写的代码都没有经过编译测试!肯定有许多漏洞,所以并不适合于直接使用,而且都没有进行参数的合法性检测,大家应该在理解的基础上编写自己的代码,而且正式使用的应该是非常健壮的代码 —— 有内置的错误检测机制。

CPU 和 UniCode

现在先来说一下 CPU 和 UniCode 的关系。

我说过,如果一个英文字符转换成 UniCode的话,在它的前面加入“00h”就可以了,所以“A”的 UniCode 码就是“0041h”,不过我们见到的在可执行文件中的 UniCode 一般前后都有“00h”,那么,究竟“A”的 UniCode 的字节方式表示是“00 41”还是“41 00”呢?是“41 00”,为什么呢?嗯~~,这都是 Inter 惹的祸。

Motolola 的 CPU(用于苹果机)的寄存器处理数据时是高位在前,比如 Long 型的数据 260 ,Motolola 的 CPU 把它输出为 Byte(因为内存是以 Byte 为单位的)的时候就是 00 00 01 04 ;而 Inter 的 CPU 处理的时候却是高位在后,所以同一 Long 型的数据 260 ,Inter 的 CPU 把它输出为 Byte 时就是 04 01 00 00 了。上一次曾经说过,标准的 UniCode的处理方法不是使用字节(Byte)的方式,而是使用字(Integer,以 VB 为准。C 语言是 WORD 或 Short)的方式,所以比如“A”的 UniCode 码“0041h”输出到内存或磁盘的时候就会改变顺序,变成“41 00”了。

知道了 CPU对处理数据的方式,在做汉化之类的工作的时候会有帮助,除此之外,对于修改游戏也有帮助(很有一些人认为游戏是不入流的东西,不过从游戏也同样能学到不少知识,比正规教学也不会少了) 。首先,因为很多程序员喜欢使用“int”数据类型,因为它是当前操作系统处理的缺省数据类型,速度很快,作为 DOS 之类的 16 位操作系统来说,“int”是 16 位的,也就是表示两个字节,而作为 Windows 9x 之类的 32 位操作系统来说,“int”是 32 位的,就是四个字节,所以我们在查找地址的时候大可以直接查找四字节数据,会大大减少查找次数的。另外,比如,我们用一种游戏修改器(比如 GE6.0)找到了一个内存地址,可以使用此工具提供的功能来直接修改此地址的值,但是也可以自己直接编辑内存,这时,如果我们要把此地址改成 100000 ,首先使用计算器得到 100000 的十六进制值是“186A0h”,把它反过来,就是“A0 86 01 00”,修改内存地址的内容,再回到游戏,如何?(可以使用这种方法验证我的说法,虽然我认为我的说法肯定是正确的,不过大家还是自己验证一下才印象深刻吧 :)

对于 VB 程序员,知道这一点也是有用的,虽然世纪编程中间很可能根本用不上,但是知识的积累才是质变的基础。

什么是字符串

字符串是什么呢?在 C 语言中是一个以“00”结尾的的字符序列,比如“String”在内存中(编译后)就是这样了:


'S' 't' 'r' 'i' 'n' 'g' 00

其中的黄底红字是字符串结尾的判断符,所以,在C语言中,如果需要求得字符串的长度的话,一般是从首地址开始,判断以后的每一个字节是否为“00”,并且做一个计数器,遇到“00”之后,计数器的计数就是字符串的长度了:


StrLen(char *S)
{
	int n=0;
	while(S[n++]!=0){}
	return n;
}


需要注意的是,有一种说法认为 C 格式的字符串是开头和结尾都有“00”的字符序列,像以下这个样子:


00 'S' 't' 'r' 'i' 'n' 'g' 00

这种说法是不正确的。只要知道C语言处理字符串的方式就可以很明确的抛弃这种说法了。不过大多数情况确实很像这样,其原因就是C语言一般把字符串统一放在一个地方,所以开头那个“00”其实是上一个字符串的结尾标志,或者也可能是其它的数据正好是“00”,比如一个比较小的长整数,其最高位就是“00”,而 Inter又把最高位放在最后... 反正和目前这个字符串无关。当然,大多数情况确实如此,所以我们确实可以把它当作一种标志,不过既然不总是这样,我们在使用这种方法查找的时候就要注意,心里一定要清楚为什么可以这样做,还要清楚我们很可能错过了一些字符串!

但是,VB 中的字符串就不同了,而且,因为版本不同,字符串的格式也不同。VB4和以前的版本的 VB 的字符串格式很复杂,它使用连续指针的方式,源指针指向一个由字符串长度和字符串首地址指针组合而成的结构,其中的字符串首地址指针再指向字符串;VB5 及其以上版本则是单指针方式,源指针指向一个复合字串,此字串开始的四个字节组成一个长整数(在 Windows 9x 上,long 和 int 的长度都是 32 位),指示此字符串的长度,其后是字符串,而且这个字符串除了应有的字符外,结尾还有两个 “00”结尾,而最后的这两个“00”不计算在字符串长度之内。一个长整数,如果是无符号的,则其取值范围为 0 ~ 4G,如果是有符号的,则取值范围为 -2G ~ 2G,因为在 VB 中除了 Byte 之外没有无符号数,所以 VB 中的字符串长度指示器的取值范围也是 -2G ~ 2G,因为不存在长度为负数的情况,所以 VB 中字符串的长度就是 0 ~ 2G 了,关于字符串的长度限制在 VB 的帮助里有介绍,不过我看到 Delphi 的帮助里说,Delphi 的字符串长度限制是 0 ~ 3G,像这种前不着村,后不着店的限制,我就不是很理解了,不过至少在小于 2G 的时候,Delphi 的字符串和 VB 的字符串的判断方法是一样的。

另外,VB 中的字符串是 UniCode 格式的,所以一个英文的 VB 字串“String”在内存中(编译后)应该是这样的:


0C 00 00 00 'S' 00 't' 00 'r' 00 'i' 00 'n' 00 'g' 00 00 00

其中红底黄字是表示此字串长度的长整数;紧跟着的是 UniCode 字串;最后的黄底红字是结尾符。

需要注意的是,因为在 VB字符串中指示长度的长整数其实指示的是字节数,而 VB 的字串是 UniCode 格式的,所以其实此数只能是偶数 —— 只在程序中使用了 StrConv 把 UniCode 转换成 ANSI 格式时除外,不过此情况只出现在程序运行时。那么为什么不指示UniCode字数呢?这是因为我们在很多情况下还需要将 UniCode 字串转换成普通的 ASCII 字符串,兼容这种情况时指示字节数更方便。

另外,让我们考虑一下:在编辑可执行文件的时候是否所有的“00”都能任我们驱使呢?不能!想象一个 VB 的空字串,因为不包含任何数据,所以它是如下的样子:


0C 00 00 00 00 00

我们得到的是一个由六个“00”组成的序列,不过这六个“00”都不能有任何修改,否则将导致程序不能正常运行。当这样一个序列正好在一个有数据的字串之后,而对那个字串的汉化需要额外的空间的时候,我们会很有使用这些“00”的冲动,不过一定要充分的考虑一下后果!

长度 2G 在十六进制中为“7fffffffh”,反过来就是“FF FF FF 7F”,所以如果有一个字串真有 2G 那么大的话,格式应该这样:


0C 00 00 00 'S' 00 't' 00 'r' 00 …… 'i' 00 'n' 00 'g' 00 00 00

从 VB 字串的格式我们可以知道,取得 VB 字串的长度不需要检索整个字串,只需要取得开头的长整数就可以了:


StrLen(char *S)
{
	return *((int *)S);
}


不过这样取得的是字符个数,相当于 LenB 函数,而 Len 函数需要把所得的数再除以 2 ,如下:


StrLen(char *S)
{
	return (*((int *)S))>>1;
}


这样,在 VB 中取得字串的长度比在 C 语言中快得多,不过只是 C ,而不是 C++ ,因为 C++ 中使用的 String 类应该和 VB 中使用的字串格式基本相同,否则 —— 哎!还是不要否则好了。 :)

UTF-8 和 UTF-16

以下的部分大多数内容摘自两只老虎网站编写的《无废话 XML》,原书讲解比我这里详细的多,建议如果可能的话,还是读一下的好。

首先说一下,一个字节能表示 2^8 = 256 种符号,不过 ANSI 定义的只有 0 ~ 127 ,127 之后的符号是扩展的,并不兼容,Dos中许多程序使用扩展的制表符制表,所以在中文 Windows 的 Dos Box 中就显示的不正常了;而两个字节能表示 2^16 = 65536 种的符号,所以 UniCode就能表示 65536 种符号,不过因为扩充的需要,事实上只使用了三四万,并保留了一个指定的空间用来在各种程序中自由使用,具体有那些,我就不说了。

所谓 UTF-16 格式,虽然称之为变换格式,但是其变换方式很简单,基本和 UniCode的格式一样,通常情况下并不需要变换,所以就不多说了。UTF-8 格式比较不同,其编码为 1 ~ 3 个字节不等,其排列格式如下:


单字节 U+0000 ~ U+007F 0 0 0 0 0 0 0 0
双字节 U+0080 ~ U+07FF 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0
三字节 U+0080 ~ U+FFFF 1 1 1 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0

其中的格式表示以位(Bit)为单位,红底黄字的是必须的格式,而黄底的是真正的编码,也就是说,单字节有 7 位可编码范围,双字节有 11 位可编码范围,三字节有 16 位可编码范围;因为单字节格式的范围正好是ASCII码的范围,而且其首位为零,所以和普通的ASCII的格式是一模一样的,也就是说,普通的英文字符串不需要进行转换就符合 UTF-8格式,所以应该说是一种和英文兼容性最好的编码,也所以在 IE5 中加入了 “总以 UTF-8 发送 URL”,不过可惜的是,我们常用的中文 URL 其实不是 UTF-8 格式,而是 GBK 格式,硬要把 GBK 当成 UTF-8 来用,也难怪会出错了。

关于 UTF-8 格式的使用,应该在文件处理上很有用处,至于以后会是 UTF-8 还是 UTF-16 占主流我不太清楚,不过即使不占主流的格式应该也有一定的支持者,了解一下也不为过。

抱歉!评论已关闭.