VB6拾遗:IUnknown接口与COM对象
http://demon.tw/programming/vb6-repick-iunknown-com.html
VB6是建立在COM之上的,所有的COM对象都必须实现IUnknown接口。
IUnknown接口的IDL定义如下:
VB6是建立在COM之上的,所有的COM对象都必须实现IUnknown接口。
IUnknown接口的IDL定义如下:
interface IUnknown
{
HRESULT QueryInterface(
[in] REFIID riid,
[out, iid_is(riid)] void **ppvObject);
ULONG AddRef();
ULONG Release();
}
QueryInterface用于接口查询,AddRef和Release用于引用计数。
COM规范规定任何组件、任何接口都必须从IUnknown继承,IUnknown包含三个函数,分别是 QueryInterface、AddRef、Release。这三个函数是无比重要的,而且它们的排列顺序也是不可改变的。
QueryInterface用于查询组件实现的其它接口,说白了也就是看看这个组件的父类中还有哪些接口类,AddRef用于增加引用计数,Release用于减少引用计数。引用计数也是COM中的一个非常重要的概念。大体上简单的说来可以这么理解,COM组件是个DLL,当客户程序要用它时就要把它装到内存里。另一方面,一个组件也不是只给你一个人用的,可能会有很多个程序同时都要用到它。但实际上DLL只装载了一次,即内存中只有一个COM组件,那COM组件由谁来释放?由客户程序吗?不可能,因为如果你释放了组件,那别人怎么用,所以只能由COM组件自己来负责。所以出现了引用计数的概念,COM维持一个计数,记录当前有多少人在用它,每多一次调用计数就加一,少一个客户用它就减一,当最后一个客户释放它的时侯,COM知道已经没有人用它了,它的使用已经结束了,那它就把它自己给释放了。
引用计数是COM编程里非常容易出错的一个地方,但所幸VC的各种各样的类库里已经基本上把AddRef的调用给隐含了,在我的印象里,我编程的时侯还从来没有调用过AddRef,我们只需在适当的时侯调用Release。至少有两个时侯要记住调用Release,第一个是调用了 QueryInterface以后,第二个是调用了任何得到一个接口的指针的函数以后,记住多查MSDN 以确定某个函数内部是否调用了AddRef,如果是的话那调用Release的责任就要归你了。 IUnknown的这三个函数的实现非常规范但也非常烦琐,容易出错,所幸的事我们可能永远也不需要自己来实现它们。
COM是很复杂的东西,不是一两句话能够说得清楚的,感兴趣的话可以看看《COM原理与应用》、《COM技术内幕》、《COM本质论》等书籍。
VB6将复杂的COM都隐藏起来了,使得COM对象使用起来非常的简单:
Sub Main() Dim o1 As Collection Dim o2 As Object Dim o3 As Control Set o1 = New Collection Set o2 = o1 Set o3 = o1 End Sub
生成的汇编代码如下:
00401891 push ___vba@095E3C24 ; /Arg1 = Project1.___vba@095E3C24
00401896 call ___vbaNew ; \MSVBVM60.__vbaNew
0040189B push eax ; /Arg2
0040189C lea eax, [ebp-14] ; |
0040189F push eax ; |Arg1 => offset LOCAL.5
004018A0 call ___vbaObjSet ; \MSVBVM60.__vbaObjSet
004018A5 push dword ptr [ebp-14] ; /Arg2 => [LOCAL.5]
004018A8 lea eax, [ebp-18] ; |
004018AB push eax ; |Arg1 => offset LOCAL.6
004018AC call ___vbaObjSetAddref ; \MSVBVM60.__vbaObjSetAddref
004018B1 push ___vba@095E3C38 ; /Arg2 = Project1.___vba@095E3C38
004018B6 push dword ptr [ebp-14] ; |Arg1 => [LOCAL.5]
004018B9 call ___vbaCastObj ; \MSVBVM60.__vbaCastObj
004018BE push eax ; /Arg2
004018BF lea eax, [ebp-1C] ; |
004018C2 push eax ; |Arg1 => offset LOCAL.7
004018C3 call ___vbaObjSet ; \MSVBVM60.__vbaObjSet
004018C8 push 004018E6
004018CD lea ecx, [ebp-14]
004018D0 call @__vbaFreeObj ; [MSVBVM60.__vbaFreeObj
004018D5 lea ecx, [ebp-18]
004018D8 call @__vbaFreeObj ; [MSVBVM60.__vbaFreeObj
004018DD lea ecx, [ebp-1C]
004018E0 call @__vbaFreeObj ; [MSVBVM60.__vbaFreeObj
__vbaNew函数用于创建对象,__vbaObjSet用于将对象变量指向内存中的对象。
当用一个对象变量给另一个对象变量赋值时,如果两边变量的类型相同,Set赋值时使用__vbaObjSetAddref函数,使其指向同一个对象,并调用该对象的AddRef函数增加引用计数;如果变量的类型不同,Set赋值前调用__vbaCastObj函数,__vbaCastObj内部调用对象的QueryInterface查询对象是否实现了相应的接口,实现了的话则用__vbaObjSet赋值,否则抛出异常“类型不匹配”。
当变量超出作用范围时需要调用__vbaFreeObj函数,与函数的名称不符,该函数并不一定会释放对象所占用的内存,而只是调用对象的Release函数减少引用计数,只有当对象的引用计数为0时对象所占的内存才会被释放。
vb中有指针吗?(实例为双向循环链表)
https://zhidao.baidu.com/question/47259296.html
VB的指针挺简单的,用着也很方便,其实对象变量就可以看成是指针,当你用Set A=Obj时,A就是指向Obj的地址,不用API就可以,当然用API可以实现更为高级的结构。
给一个例子,一个用VB实现的双向循环链表。有链表的生成,删除和结点的插入。
先定义一个结点类,类名为Node,代码为:
Option Explicit
Public pNext As Node
Public pPrev As Node
Public data As Single
Private Sub Class_Initialize()
Set pNext = Nothing
Set pPrev = Nothing
End Sub
Private Sub Class_Terminate()
Set pNext = Nothing
Set pPrev = Nothing
End Sub
再添加一个窗体,窗体上添加两个列表框,list1和list2,窗体的代码为:
Option Explicit
Private pHead As Object
Private pV As Object
Private Sub Form_Load()
Dim i As Integer
Set pHead = New Node
Call CreateLinkList
Call InsertNode(pHead, 503)
Call InsertNode(pHead, 1.875)
Call InsertNode(pHead, -3.675)
For i = 1 To 100
Call InsertNode(pHead, -1 * i)
Next
Call PrintList
Call DeleteList
End Sub
Public Sub CreateLinkList()
Dim p As Node
Dim nLoop As Integer
Static pLast As Node
pHead.data = 0
Set pLast = pHead
For nLoop = 1 To 501
Set p = New Node
p.data = nLoop
Set pLast.pNext = p
Set p.pPrev = pLast
Set pLast = p
Next
Set pLast = Nothing
Set p.pNext = pHead
Set pHead.pPrev = p
Exit Sub
End Sub
Public Sub PrintList()
List1.AddItem "Forwards"
Set pV = pHead
Do
List1.AddItem pV.data
Set pV = pV.pNext
Loop While Not pV Is pHead
List2.AddItem "Backwards"
Set pV = pHead.pPrev
Do
List2.AddItem pV.data
Set pV = pV.pPrev
Loop While Not pV Is pHead.pPrev
End Sub
Public Sub DeleteList()
Dim p As Node
Set pV = pHead
Do
Set pV = pV.pNext
Set p = pV.pPrev
If Not p Is Nothing Then
Set p.pNext = Nothing
Set p.pPrev = Nothing
End If
Set p = Nothing
Loop While Not pV.pNext Is Nothing
Set pV = Nothing
Set pHead = Nothing
End Sub
Public Sub InsertNode(head As Node, data As Single)
Dim p As New Node, q As Node, prev As Node
p.data = data
Set q = head
Set prev = head.pPrev
While ((q.data < p.data) And Not q.pNext Is head)
Set q = q.pNext
Set prev = prev.pNext
Wend
If Not q.pNext Is head Then
Set p.pNext = q
Set p.pPrev = prev
Set prev.pNext = p
Set q.pPrev = p
If q Is head Then
Set head = p
End If
Else
Set p.pNext = head
Set p.pPrev = q
Set head.pPrev = p
Set q.pNext = p
End If
End Sub
一个双向循环链表就形成了,List1中是正向遍历的结果,List2中是反向遍历的
结果类的构造器Class_Initialize()过程,类的析构Class_Termainate()过程,结点内存的分配和回收都由类自身完成,还有多态,pHead As Object;Set pHead =New Node;Set pHead.pPrev = p;指向基类的指针指向了子类,并调用了子类的属性,是不是挺像C++的代码?
链表有了,二叉树,由临接表构成的图等数据结构都很容易实现了吧,实际上用VB能构造很复杂的数据结构,上面的代码只是简单的示例,实际可以做的更完善。
另外,VB6也能够生成真实的地址。三种未正式公布的VBA方法VarPtr,ObjPtr,和StrPtr(实际上是指向运行DLL同一入口的三个不同的类型库别名)就可以用来建立指针,使用address=ObjPtr(Obj)就可以获得对象的地址,Obj为需要地址的对象,而Address为一个long型变量,其中放置了对象的地址,使用VarPtr(产生变量的地址和UDT),StrPtr(产生字符串的地址)和ObjPtr (产生对象的地址)可以构造真实的,非常复杂的数据结构。
上面三个方法并没有在Microsoft的正式文档资料中公布(包括MSDN),但查看VB6的基本动态运行库MSVBVM60.DLL可以发现这三个方法:
[entry(0x60000006),hidden]
long __stdcall VarPtr([in]void* Ptr);
[entry(0x60000007),hidden]
long __stdcall StrPtr([in]BSTR Ptr);
[entry(0x60000008),hidden]
long __stdcall ObjPtr([in]IUnknown* Ptr);
类似这样的隐藏方法还有不少,实际上VB6的功能是相当强大的
VB6编程技术大全中实例(并非原文,稍微编辑一下)
§ 7-4-2 集合类
集合类保存了私有集合变量的引用,并连接到外部界面,以使客户代码相信它正在交互处理真正的Collection。基于之上的目的,要增强已有的类(Invoice)的功能,就要再建立一个集合类(InvoiceLines)
Private m_InvoiceLines as New Collection
Sub Add( newItem As CinvoiceLine, Optional Key As Variant, Optional Before As Variant, Optional After As Variant,)
m_InvoiceLines.Add newItem, Key
End Sub
Sub Remove( index As Variant,)
m_InvoiceLines.Remove index
End Sub
Function Item(index As Variant) As CInvoiceLine
Set Item = m_InvoiceLines.Item(index)
End Function
Property Get Count( ) As Long
Count = m_InvoiceLines.Count
End Property
还要做两件事,才能让集合类完美模仿标准的Collection:1)支持缺省项,2)支持枚举项
1)使Item成为缺省成员
当处理Collection对象时,代码中常常省略Item成员的名字。为了在集合类中也支持此特性,只需要让Item成为类的缺省成员,可以从Tools主菜单中选择Procedure Attribute,在对话框顶部的组合框中选择Item,扩展对话框(即Procedure Attribute对话框下端显示增加选项),并在ProcID字段中键入或在下拉式列表中选择0(缺省)
在§ 6-2-5 属性 一节中
2、类的缺省成员
大多数VB控件及内部对象都有一个缺省属性或者方法。例如Collection有一个缺省的Item方法。如果在表达式中省略了成员名,VB这是指特定的对象,所以这种数据项认为是缺省成员。按照下列方法,甚至可以对自己的类实现同样的机制:
a、在代码窗口中的属性或者方法定义处单击鼠标,从“工具Tools”中调用过程“属性Attribute”,然后如果数据项的名字还没有显示,就在组合框的最顶端选择数据项的名字。
b、另一方面,按下F2打开Object Browser,在最左端的窗格中选择类模块的名字:在最右端的窗格中,在要成为缺省成员的数据项上单击鼠标,然后从出现的弹出菜单中选择Property,如图6-6
c、一旦所感兴趣的数据项以高亮方式显示在最顶端的Name组合框中,单击“高级Advanced”按钮,扩展Procedure Attribute对话框,如图6-7
d、在Procedure ID组合框中选择“缺省default”项;另一方面还可以在组合框的编辑区内输入0
e、单击OK按钮,确定生效并关闭对话框。在Object Browser中,在该成员名旁边已经出现一个小圆指示符。这证明它已经成为类的缺省成员。
一个类只能有一个缺省方法或者属性。如果出现第二个缺省项,报错!
2)为枚举项增加支持
首先添加下面的过程到类模块中
Function NewEnum( ) As IUnknown
Set NewEnum = m_InvoiceLines.[_NewEnum]
End Function
然后激活Procedure Attribute对话框,选择NewEnum成员,为它分配ProcID等于-4,选择复选框Hide This Member,关闭对话框
OLE协议规定类必须通过一个ProcID等于-4的函数提供此枚举器对象
关于《过程属性对话框》
Setting the Procedure Attributes
https://msdn.microsoft.com/en-us/library/aa260636(v=vs.60).aspx