VB真是想不到系列之三:VB指针葵花宝典之函数指针

类别:VB语言 点击:0 评论:0 推荐:

《VB真是想不到系列》
    每次看大师的东西到了精彩之处,我就会拍案叫绝:"哇噻,真是想不到!"。在经过很多次这种感慨之后,我发现只要我们动了脑筋,我们自己也能有让别人想不到的东西。于是想到要把这些想不到的东拿出来和大家一起分享,希望抛砖引玉,能引出更多让人想不到的东西。
本系列文章可见:
    http://www.csdn.net/develop/list_article.asp?author=AdamBear

               VB真是想不到系列之三:VB指针葵花宝典之函数指针
关键字:VB、HCAK、指针、函数指针、效率、数组、对象、排序
难度:中级
要求:熟悉VB,了解基本的排序算法,会用VC更好。

引言:   
    不知大家在修习过本系列第二篇《VB指针葵花宝典》后有什么感想,是不是觉得宝典过于偏重内功心法,而少了厉害的招式。所以,今天本文将少讲道理,多讲招式。不过,还是请大家从名门正派的内功心法开始学起,否则会把九阴真经练成九阴白骨爪。
    今天,我们重点来谈谈函数指针的实际应用。
    接着上一篇文章,关于字串的问题,听CSDN上各位网友的建议,我不准备写什么《VB字符串全攻略》了,关于BSTR的结构,关于调用API时字串在UNICODE和ANSI之间的转换问题,请参考MSDN的Partial Books里的《Win32 API Programming with Visual Basic》里的第六章《Strings》。今天就让我们先忘掉字符串,专注于函数指针的处理上来。

    一、函数指针
    AddressOf得到一个VB内部的函数指针,我们可以将这个函数指针传递给需要回调这个函数的API,它的作用就是让外部的程序可以调用VB内部的函数。
    但是VB里函数指针的应用,远不象C里应用那么广泛,因为VB文档里仅介绍了如何将函数指针传递给API以实现回调,并没指出函数指针诸多神奇的功能,因为VB是不鼓励使用指针的,函数指针也不例外。
    首先让我们对函数指针的使用方式来分个类。
    1、回调。这是最基本也是最重要的功能。比如VB文档里介绍过的子类派生技术,它的核心就是两个API:SetWindowLong和CallWindowProc。
    我们可以使SetWindowLong这个API来将原来的窗口函数指针换成自己的函数指针,并将原来的窗口函数指针保存下来。这样窗口消息就可以发到我们自己的函数里来,并且我们随时可以用CallWindowProc来调用前面保存下来的窗口指针,以调用原来的窗口函数。这样,我们可以在不破坏原有窗口功能的前提下处理钩入的消息。
    具体的处理,我们应该很熟悉了,VB文档也讲得很清楚了。这里需要注意的就是CallWindowProc这个API,在后面我们将看到它的妙用。
    在这里我们称回调为让"外部调用内部的函数指针"。
    2、程序内部使用。比如在C里我们可以将C函数指针作为参数传递给一个需要函数指针的C函数,如后面还要讲到的C库函数qsort,它的声明如下:
    #define int (__cdecl *COMPARE)(const void *elem1, const void *elem2)
    void qsort(void *base, size_t num, size_t width,
         COMPARE pfnCompare);
它需要一个COMPARE类型函数指针,用来比较两个变量大小的,这样排序函数可以调用这个函数指针来比较不同类型的变量,所以qsort可以对不同类型的变量数组进行排序。
    我们姑且称这种应用为"从内部调用内部的函数指针"。
    3、调用外部的函数
    也许你会问,用API不就是调用外部的函数吗?是的,但有时候我们还是需要直接获取外部函数的指针。比如通过LoadLibrary动态加载DLL,然后再通过GetProcAddress得到我们需要的函数入口指针,然后再通过这个函数指针来调用外部的函数,这种动态载入DLL的技术可以让我们更灵活的调用外部函数。
    我们称这种方式为"从内部调用外部的函数指针"
    4、不用说,就是我们也可控制"从外部调用外部的函数指针"。不是没有,比如我们可以加载多个DLL,将其中一个DLL中的函数指针传到另一个DLL里的函数内。
    上面所分的"内"和"外"都是相对而言(DLL实际上还是在进程内),这样分类有助于以后我们谈问题,请记住我上面的分类,因为以后的文章也会用到这个分类来分析问题。

    函数指针的使用不外乎上面四种方式。但在实际使用中却是灵活多变的。比如在C++里继承和多态,在COM里的接口,都是一种叫vTable的函数指针表的巧妙应用。使用函数指针,可以使程序的处理方式更加高效、灵活。
    VB文档里除了介绍过第一方式外,对其它方式都没有介绍,并且还明确指出不支持“Basic 到 Basic”的函数指针(也就是上面说的第二种方式),实际上,通过一定的HACK,上面四种方式均可以实现。今天,我们就来看看如何来实现第二种方式,因为实现它相对来说比较简单,我们先从简单的入手。至于如何在VB内调用外部的函数指针,如何在VB里通过处理vTable接口函数指针跳转表来实现各种函数指针的巧妙应用,由于这将涉及COM内部原理,我将另文详述。
    其实VB的文档并没有说错,VB的确不支持“Basic 到 Basic”的函数指针,但是我们可以绕个弯子来实现,那就是先从"Basic到API",然后再用第一种方式"外部调用内部的函数指针"来从"API到BASIC",这样就达到了第二种方式从"Basic 到 Basic"的目的,这种技术我们可以称之为"强制回调",只有VB里才会有这种古怪的技术。
    说得有点绕口,但是仔细想想窗口子类派生技术里CallWindowProc,我们可以用CallWindowProc来强制外部的操作系统调用我们原来的保存的窗口函数指针,同样我们也完全可以用它来强制调用我们内部的函数指针。
    呵呵,前面说过要少讲原理多讲招式,现在我们就来开始学习招式吧!
    考虑我们在VB里来实现和C里一样支持多关键字比较的qsort。完整的源代码见本文配套代码,此处仅给出函数指针应用相关的代码。   
    '当然少不了的CopyMemory,不用ANY的版本。
    Declare Sub CopyMemory Lib "kernel32" Alias _
"RtlMoveMemory" (ByVal dest As Long, ByVal source As Long, _
                 ByVal numBytes As Long)

    '嘿嘿,看下面是如何将CallWindowProc的声明做成Compare声明的。
    Declare Function Compare Lib "user32" Alias _
"CallWindowProcA" (ByVal pfnCompare As Long, ByVal pElem1 As Long, _
                   ByVal pElem2 As Long, ByVal unused1 As Long,  _
                   ByVal unused2 As Long) As Integer
'注:ByVal xxxxx As Long ,还记得吧!这是标准的指针声明方法。
    
    '声明需要比较的数组元素的结构
    Public Type TEmployee
        Name As String
        Salary As Currency
    End Type 

    '再来看看我们的比较函数
    '先按薪水比较,再按姓名比较
    Function CompareSalaryName(Elem1 As TEmployee, _
                               Elem2 As TEmployee, _                                                  
                               unused1 As Long,  _
                               unused2 As Long) As Integer
        Dim Ret As Integer
        Ret = Sgn(Elem1.Salary - Elem2.Salary)
        If Ret = 0 Then
            Ret = StrComp(Elem1.Name, Elem2.Name, vbTextCompare)
        End If
        CompareSalaryName = Ret
    End Function
    '先按姓名比较,再按薪水比较
    Function CompareNameSalary(Elem1 As TEmployee, _
                               Elem2 As TEmployee, _
                               unused1 As Long,  _
                               unused2 As Long) As Integer
        Dim Ret As Integer
        Ret = StrComp(Elem1.Name, Elem2.Name, vbTextCompare)
        If Ret = 0 Then
            Ret = Sgn(Elem1.Salary - Elem2.Salary)
        End If
        CompareNameSalary = Ret
    End Function

    最后再看看我们来看看我们最终的qsort的声明。
    Sub qsort(ByVal ArrayPtr As Long, ByVal nCount As Long, _
              ByVal nElemSize As Integer, ByVal pfnCompare As Long)
    上面的ArrayPtr是需要排序数组的第一个元素的指针,nCount是数组的元素个数,nElemSize是每个元素大小,pfnCompare就是我们的比较函数指针。这个声明和C库函数里的qsort是极为相似的。
    和C一样,我们完全可以将Basic的函数指针传递给Basic的qsort函数。
    使用方式如下:
    Dim Employees(1 To 10000) As TEmployee
    '假设下面的调用对Employees数组进行了赋值初始化。
    Call InitArray()
    '现在就可以调用我们的qsort来进行排序了。
    Call qsort(VarPtr(Employees(1)), UBound(Employees), _
               LenB(Employees(1)), AddressOf CompareSalaryName)
    '或者先按姓名排,再按薪水排
    Call qsort(VarPtr(Employees(1)), UBound(Employees), _
               LenB(Employees(1)), AddressOf CompareNameSalary)   

    聪明的朋友们,你们是不是已经看出这里的奥妙了呢?作为一个测验,你能现在就给出在qsort里使用函数指针的方法吗?比如现在我们要通过调用函数指针来比较数组的第i个元素和第j个元素的大小。
    没错,当然要使用前面声明的Compare(其实就是CallWindowProc)这个API来进行强制回调。
    具体的实现如下:
    Sub qsort(ByVal ArrayPtr As Long, ByVal nCount As Long, _
              ByVal nElemSize As Integer, ByVal pfnCompare As Long)
        Dim i As Long, j As Long
        '这里省略快速排序算法的具体实现,仅给出比较两个元素的方法。
        If Compare(pfnCompare, ArrayPtr + (i - 1) * nElemSize, _
                   ArrayPtr + (j - 1) * nElemSize, 0, 0) > 0 Then
            '如果第i个元素比第j个元素大则用CopyMemory来交换这两个元素。
        End IF
    End Sub  

    招式介绍完了,明白了吗?我再来简单地讲解一下上面Compare的意思,它非常巧妙地利用了CallWindowProc这个API。这个API需要五个参数,第一个参数就是一个普通的函数指针,这个API能够强马上回调这个函数指针,并将这个API的后四个Long型的参数传递给这个函数指针所指向的函数。这就是为什么我们的比较函数必须要有四个参数的原因,因为CallWindowProc这个API要求传递给的函数指针必须符合WndProc函数原形,WndProc的原形如下:
    LRESULT (CALLBACK* WNDPROC) (HWND, UINT, WPARAM, LPARAM);
    上面的LRESULT、HWND、UINT、WPARAM、LPARAM都可以对应于VB里的Long型,这真是太好了,因为Long型可以用来作指针嘛!
    再来看看工作流程,当我们用AddressOf CompareSalaryName做为函数指针参数来调用qsort时,qsort的形参pfnCompare被赋值成了实参CompareSalaryName的函数指针。这时,调用Compare来强制回调pfnCompare,就相当于调用了如下的VB语句:
     Call CompareSalaryName(ArrayPtr + (i - 1) * nElemSize, _
                            ArrayPtr + (j - 1) * nElemSize, 0, 0)
     这不会引起参数类型不符错误吗?CompareSalaryName的前两个参数不是TEmployee类型吗?的确,在VB里这样调用是不行的,因为VB的类型检查不会允许这样的调用。但是,实际上这个调用是API进行的回调,而VB不可能去检查API回调的函数的参数类型是一个普通的Long数值类型还是一个结构指针,所以也可以说我们绕过了VB对函数参数的类型检查,我们可以将这个Long型参数声明成任何类型的指针,我们声明成什么,VB就认为是什么。所以,我们要小心地使用这种技术,如上面最终会传递给CompareSalaryName函数的参数"ArrayPtr + (i - 1) * nElemSize"只不过是一个地址,VB不会对这个地址进行检查,它总是将这个地址当做一个TEmployee类型的指针,如果不小心用成了"ArrayPtr + i * nElemSize",那么当i是最后一个元素时,我们就会引起内存越权访问错误,所以我们要和在C里处理指针一样注意边界问题。
   
    函数指针的巧妙应用这里已经可见一斑了,但是这里介绍的方法还有很大的局限性,我们的函数必须要有四个参数,更干净的做法还是在VC或Delphi里写一个DLL,做出更加符合要求的API来实现和CallWindowProc相似的功能。我跟踪过CallWindowProc的内部实现,它要做许多和窗口消息相关的工作,这些工作在我们这个应用中是多余的。其实实现强制回调API只需要将后几个参数压栈,再call第一个参数就行了,不过几条汇编指令而已。
    正是因为CallWindowProc的局限性,我们不能够用它来调用外部的函数指针,以实现上面说的第三种函数指针调用方式。要实现第三种方式,Matt Curland大师提供了一个噩梦一般的HACK方式,我们要在VB里凭空构造一个IUnknown接口,在IUnknown接口的vTable原有的三个入口后再加入一个新入口,在新入口里插入机器代码,这个机器代码要处理掉this指针,最后才能调用到我们给的函数指针,这个函数指针无论是内部的还是外部的都一样没问题。在我们深入讨论COM内部原理时我会再来谈这个方法。
    另外,排序算法是个见仁见智的问题,我本来想,在本文提供一个最通用性能最好的算法,这种想法虽好,但是不可能有在任何情况下都“最好”的算法。本文提供的用各种指针技术来实现的快速排序方法,应该比用对象技术来实现同样功能快不少,内存占用也少得多。可是就是这个已经经过了我不少优化的快速排序算法,还是比不了ShellSort,因为ShellSort实现上简单。从算法的理论上来讲qsort应该比ShellSort平均性能好,但是在VB里这不一定(可见本文配套代码,里面也提供了VBPJ一篇专栏的配套代码ShellSort,非常得棒,本文的思想就取自这个ShellSort)。
    但是应当指出无论是这里的快速排序还是ShellSort,都还可以大大改进,
因为它们在实现上需要大量使用CopyMemroy来拷贝数据(这是VB里使用指针的缺点之一)。其实,我们还有更好的方法,那就是Hack一下VB的数组结构,也就是COM自动化里的SafeArray,我们可以一次性的将SafeArray里的各个数组元素的指针放到一个long型数组里,我们无需CopyMemroy,我们仅需交换Long型数组里的元素就可以达到实时地交换SafeArray数组元素指针的目的,数据并没有移动,移动的仅仅是指针,可以想象这有快多。在下一篇文章《VB指针葵花宝典之数组指针》中我会来介绍这种方法。
   

后记:
    我学习所以我快乐。
  
   

本文地址:http://com.8s8s.com/it/it6424.htm