ExcelTip.Net留存知识帖 ---【注:附件之前被网盘供应商清空后,现已修复-现已修复-现已修复为本地下载!】
现在位置:首页 > E文精选 > Excel VBA > VBA中数据存储的研究及其应用

VBA中数据存储的研究及其应用

作者:绿色风 分类: 时间:2022-08-17 浏览:147
楼主
amulee
在声明变量之后,计算机会为其在内存中开辟一个独立的空间用于存放变量。一般来说,内存中有两个地方用于存放变量:栈和堆,这两个区域是分别存放两种类型的变量:值类型和引用类型。
以下,我们针对这两种变量类型和内存空间进行详细的解释。通过本帖的学习,你将了解到:
1、 变量存储的方式
2、 栈和堆的原理
3、 如何进一步提高效率
4、 如何利用变量存储的地址来恢复自定义功能区的运行
2楼
amulee
基础篇

值类型:包括数字类型和日期类型(本质都是数字)。这部分类型的数据有固定的内存开销,这些类型的变量存放在栈中,并且直接将变量的值存放于其中。引用类型:通常为对象和文本字符串(String类型,稍后讨论)。这部分类型的数据一般没有固定大小的内存开销,其存储分为两部分:变量真实的值存放在堆中,在栈中存放了真实变量值在堆中的地址。


 





如上图所示,栈中的数据一般比较规整,但空间比较小,查找效率高;而堆中的数据比较零散,空间比较大,查找效率相对较低。对于常规的值类型的数据,其数值直接存放在栈中,而对于复杂的对象类型,在栈中存放了一个地址,通过该地址可以找到堆中该对象真实的值。

当改变栈中值类型变量的值的时候,系统直接将新的值写入栈中变量所在的空间。而当需要改变对象的值时,比如改变对象的某个属性的值,系统会先从栈中找出该对象的地址,然后根据该地址从堆中找到该对象,再在堆中改变其值。


3楼
amulee
从变量的赋值就可以看出,存储在栈中的值使用Let语句赋值,比如变量A和B:
  1. [Let] A=B

其中,Let是可以省略的。因而我们通常写成:
  1. A=B


而对于存储在堆中的数据(字符串除外),我们则使用Set语句赋值:
  1. Set A=B

使用Set语句赋值后,并非将对象B的值赋值给对象A,而是将B中原先存放的地址给了A,也就是B和A最终都指向同一个对象。


 

如上图,A原先存放了对象A的地址,其变量指向对象A,B也亦然。当执行完上述语句后,对象B的地址给了A,因而此时A指向了对象B。此后通过A进行的任何对象的操作,都将反应到B中,而不再会影响原先的对象A。
如以下程序:
  1. Sub Test1()
  2.     Dim A As Range
  3.     Dim B As Range
  4.     Set A = Range("A1") 'A指向A1单元格
  5.     Set B = Range("B1") 'A指向B1单元格
  6.     Debug.Print A.Address   '输出A1的地址
  7.     Debug.Print B.Address   '输出B1的地址
  8.     Set A = B           '将A指向B所指向的单元格,即B1
  9.     Debug.Print A.Address   '输出B1的地址
  10.     Debug.Print B.Address   '输出B1的地址
  11. End Sub

最初,A和B指向了不同的单元格对象,而执行了Set A=B语句之后,程序将B中的地址给了A,因而使得A也指向B所指向的单元格,即B1。所以之后,A变量和B变量实际操作的都是同一个真实的对象,即B1单元格。
4楼
amulee
进阶篇
我们已经知道了值类型放在栈中,而引用类型中的对象其实际值放在堆中。值类型的变量用Let语句赋值,而且引用类型的变量用Set语句赋值,那么文本字符串类型(String)用Let语句赋值,为何其真实值放在堆中的呢?而数组和自定义类型(Type结构)呢?
事实上,内存中栈的容量一般都很小,VBA中的栈只有1M大小。因而栈中一般存放基本的值类型数据,因为这类数据尺寸固定,占内存空间一般都不大。以Double类型为例,其占内存8个字节,对于1M的栈而言,可以声明13万个该类型的变量,对于一般的程序而言不太可能用不完。
而对于对象、数组、文本字符串等类型,由于其尺寸一般无法确定,而且其尺寸一般可以很大。例如一个对象可以包含方法,属性,事件等许多内容,占内存一般比较大。特别是相对复杂的对象,如Range对象,若将其放入栈中,则栈很快就会被占满。因而此类对象一般都放在堆中。
再来看下文本字符串,一个文本字符串长度可以达到2G,远远超过了栈中1M的大小限制,所以无法将其放入栈中,只能放入堆中。

接着我们来验证一下之前所论述的理论。首先,我们需要介绍3个VBA中隐藏的函数:VarPtr, StrPtr和ObjPtr。这三个函数都可以用来获取变量的地址,其语法为:
  1. VarPtr(Ptr As Any) As LongPtr
  2. StrPtr(Ptr As String) As LongPtr
  3. ObjPtr(Ptr As Object) As LongPtr

其中VarPtr返回变量的地址,对于值类型的变量,返回其真实的地址,对于引用类型的变量,则返回该变量存放地址的内存空间的地址(非变量的真实数值地址)。
StrPtr返回文本字符串真实存放的地址。
ObjPtr返回真实存放对象变量空间的地址。
5楼
amulee
我们编写一小段程序Test2,先看以下基本的数据类型的存储情况。
  1. Sub Test2()
  2.     Dim mByt As Byte
  3.     Dim mInt As Integer
  4.     Dim mLng As Long
  5.     Dim mSng As Single
  6.     Dim mDbl As Double
  7.     Dim mCur As Currency
  8.     Dim mDte As Date
  9.     Dim mObj As Object
  10.     Dim mVrt As Variant
  11.     Dim mSt1 As String
  12.     Dim mSt2 As String * 6
  13.     Debug.Print "Byte    :" & vbTab & _
  14.                     "VarPtr:" & VarPtr(mByt) & vbTab & _
  15.                     "StrPtr:" & StrPtr(mByt) & vbTab '& _
  16.                     "ObjPtr:" & ObjPtr(mByt)
  17.     Debug.Print "Integer :" & vbTab & _
  18.                     "VarPtr:" & VarPtr(mInt) & vbTab & _
  19.                     "StrPtr:" & StrPtr(mInt) & vbTab '& _
  20.                     "ObjPtr:" & ObjPtr(mint)
  21.     Debug.Print "Long    :" & vbTab & _
  22.                     "VarPtr:" & VarPtr(mLng) & vbTab & _
  23.                     "StrPtr:" & StrPtr(mLng) & vbTab '& _
  24.                     "ObjPtr:" & ObjPtr(mlng)
  25.     Debug.Print "Single  :" & vbTab & _
  26.                     "VarPtr:" & VarPtr(mSng) & vbTab & _
  27.                     "StrPtr:" & StrPtr(mSng) & vbTab '& _
  28.                     "ObjPtr:" & ObjPtr(msng)
  29.     Debug.Print "Double  :" & vbTab & _
  30.                     "VarPtr:" & VarPtr(mDbl) & vbTab & _
  31.                     "StrPtr:" & StrPtr(mDbl) & vbTab '& _
  32.                     "ObjPtr:" & ObjPtr(mdbl)
  33.     Debug.Print "Currency:" & vbTab & _
  34.                     "VarPtr:" & VarPtr(mCur) & vbTab & _
  35.                     "StrPtr:" & StrPtr(mCur) & vbTab '& _
  36.                     "ObjPtr:" & ObjPtr(mCur)
  37.     Debug.Print "Date    :" & vbTab & _
  38.                     "VarPtr:" & VarPtr(mDte) & vbTab & _
  39.                     "StrPtr:" & StrPtr(mDte) & vbTab '& _
  40.                     "ObjPtr:" & ObjPtr(mDte)
  41.     Set mObj = ThisWorkbook
  42.     Debug.Print "Object  :" & vbTab & _
  43.                     "VarPtr:" & VarPtr(mObj) & vbTab & _
  44.                     "StrPtr:" & StrPtr(mObj.Name) & vbTab & _
  45.                     "ObjPtr:" & ObjPtr(mObj)
  46.     Debug.Print "Variant :" & vbTab & _
  47.                     "VarPtr:" & VarPtr(mVrt) & vbTab & _
  48.                     "StrPtr:" & StrPtr(mVrt) & vbTab '& _
  49.                     "ObjPtr:" & ObjPtr(mVrt)
  50.     Debug.Print "String1 :" & vbTab & _
  51.                     "VarPtr:" & VarPtr(mSt1) & vbTab & _
  52.                     "StrPtr:" & StrPtr(mSt1) & vbTab '& _
  53.                     "ObjPtr:" & ObjPtr(mSt1)
  54.     Debug.Print "String2 :" & vbTab & _
  55.                     "VarPtr:" & VarPtr(mSt2) & vbTab & _
  56.                     "StrPtr:" & StrPtr(mSt2) & vbTab '& _
  57.                     "ObjPtr:" & ObjPtr(mSt2)
  58. End Sub


运行该程序后,可以在立即窗口中出现类似于下图的输出



 


由于ObjPtr只对对象类型的适用,所以代码中只允许Object类型输出该地址。并且可以发现,在输出Object地址之前,对Object对象进行了赋值,否则将发生错误,并且Object的StrPtr返回的是其中一个属性Name的地址。

观察一下该输出,所有的VarPtr输出的地址段都在一个较小的地址段,其余的输出都在较大的地址段(变长的文本字符串除外,稍后讨论)。并且可以发现,变量自上而下是依次先后声明的,而地址却是由大至小输出的。其实这是由于栈的特性而决定的。
在计算机编程中,栈是一种后进先出的数据结构。可以把栈相象成一个桶,先放进去的东西会在最底下,最后放进去的东西在最上面。如图所示:


 

因而,放在栈中的变量,最先声明的变量放在了栈最下面的空间中,即栈的最后的地址中。随着变量的增加,不断放入之前的地址中,直到栈的空间挤满。
由此可见,一般的变量声明,符合栈的运作法则,无论是引用类型或者值类型,都会在栈中开辟一个空间,对于值类型而言,会开辟一个与其变量大小相匹配的空间用于存放其真实的值,而引用类型,则会开辟一个空间用于存放变量在堆中的真实地址。

对于ObjPtr函数,只有当对象变量指向某个对象后,该函数才能返回值,否则该函数将返回0(感兴趣的读者可以尝试一下)。可以发现,其返回值超过了栈的最大地址。实质上对象变量的真实值是放在堆中,堆中的地址一般没有规律,是由系统按照一定的算法自动分配的,因而会显得比较杂乱,无规律。但有个共同点就是,都是比栈大的地址。
6楼
amulee
同样,对于StrPtr函数所返回的值,可以发现也是在堆中的地址。但在本例中有两个比较奇怪的现象:对于值类型的变量,StrPtr函数同样可以返回一个值,另外变长长度的文本字符串,其返回值为0。
对于后者,其原因与对象变量一样。当变长字符串未赋值的时候,系统未真正给该变量开辟空间,因而返回0。只有当其赋值之后,才能返回地址的值。如下例。而对于定长的文本字符串,系统在最开始就为其分配了内存空间。
  1. Sub Test3()
  2.     Dim strA As String
  3.     Debug.Print StrPtr(strA)
  4.     strA = "AAA"
  5.     Debug.Print StrPtr(strA)
  6.     strA = "BBB"
  7.     Debug.Print StrPtr(strA)
  8.     strA = "BBB"
  9.     Debug.Print StrPtr(strA)
  10. End Sub

以上程序运行后,将会在立即窗口中输出类似下图的内容


 

当变量声明后,由于未实际赋值,所以在堆中没有开辟任何空间存放变量实际的值。而赋值后,系统会在堆中为其开辟空间存放变量的值。而且有趣的现象是,每次赋值,系统都会为字符分配一个新的空间,即使新的赋值没有任何变化,系统也会重新分配空间。因而字符串的操作要比数值类型的操作更费时。
再来看下面这个例子:
  1. Sub Test4()
  2.     Dim strA As String
  3.     strA = "AAAA"
  4.     Debug.Print StrPtr(strA)
  5.     Mid(strA, 2, 1) = "B"
  6.     Debug.Print StrPtr(strA)
  7. End Sub

运行完程序后,可以发现两次输出的地址一样。可以推断,使用MID方法进行字符串的替换将在原有的地址中执行,而不会去开辟新的空间。因而当需要替换字符串中字符时,可以采用该方法,执行效率相对高一些。

对于数字类型以及日期类型的变量,理论上StrPtr函数不应返回任何值,然而VBA的运行过程中会将变量的类型自动转换为所需要的类型。StrPtr函数接受的是一个String类型的文本,因而,对于其他类型的变量,VBA将会尝试将其转换为String类型的值。在这一过程中,VBA将会为其在堆中开辟一个临时的空间,StrPtr函数将返回该临时空间的地址。因而,以下语句也将得到一个堆中的地址。
  1. Msgbox StrPtr(111)
7楼
amulee
数组和自定义类型变量
根据之前的描述,我们可以大胆的假设一下数组和自定义类型存储情况,并采用上述方法进行验证。
猜测
数组变量的尺寸可以很大,并且动态数组可以随时调整尺寸,因而猜测其真实的值存放在堆中,而在栈中只存放对真实地址的引用。
而自定义类型中,其字段可以混杂着值类型和引用类型。其变量值应存储在栈中,而字段的值则根据前述的规则执行,即值类型的值存放在栈中,而引用类型的真实值存放在堆中。

验证
插入一个模块,输入以下代码并运行Test5
  1. Type MyType
  2.     Name As String
  3.     Age As Long
  4.     Height As Long
  5. End Type
  6. Sub Test5()
  7.     Dim MT As MyType
  8.     Dim arr1(10) As Long
  9.     Dim arr2

  10.     Debug.Print "Defined Type     :" & vbTab & _
  11.                     "VarPtr:" & VarPtr(MT) & vbTab & vbTab & _
  12.                     "Field1:" & VarPtr(MT.Name) & vbTab & vbTab & _
  13.                     "Field2:" & VarPtr(MT.Age) & vbTab & vbTab & _
  14.                     "Field3:" & VarPtr(MT.Height)
  15.                     
  16.     Debug.Print "Array Pre Defined:" & vbTab & _
  17.                     "VarPtr:" & VarPtr(arr1(0)) & vbTab & _
  18.                     "VarPtr:" & StrPtr(arr1(1))
  19.                     
  20.     ReDim arr2(10)
  21.     Debug.Print "Array            :" & vbTab & _
  22.                     "VarPtr:" & VarPtr(arr2) & vbTab & vbTab & _
  23.                     "1stPtr:" & VarPtr(arr2(0)) & vbTab & vbTab & _
  24.                     "2ndPtr:" & StrPtr(arr2(1))
  25. End Sub

在立即窗口中可以看见类似下图的输出:


 

可以看见自定义类型的变量存放在栈中,而且其中的值按照之前的规则执行。
很遗憾,当显示声明数组变量时(Dim Arr()),三个获取地址的函数都无法使用。其中的元素,可以发现都存储在了堆中。而当使用Variant类型的变量,之后再次Redim一个数组时,数组变量的地址存放在了栈中,而实际的数据存放在了堆中。由此可以推断数组变量在栈中存放一个堆中真实地址的引用。
8楼
amulee
小结和应用
介绍了那么多的理论知识,那么究竟有什么用途呢?从以上的论述中,我们可以发现:
1、        值类型的变量存放在栈中,其访问速度相对较快。而引用类型(对象)的访问速度较慢。因而在编写程序的时候,尽量减少对对象变量的访问。如果只是简单的字段存储,可以定义自定义类型,而避免使用类。
2、        避免反复给文本类型的变量赋值,因为每次赋值将会消耗时间去开辟新的空间。
3、        每一次的类型转换,系统都将开辟一个临时的空间用于临时存放转换的结果。因而对于数据类型,尽量在变量声明时就确定,避免使用Variant类型的变量,而造成类型转换的效率损失。

其实,获取了变量的地址,对于某些时候有极大的帮助。以下我们举一个实用的例子。
自Office 2007以来,微软采用了Ribbon替代了之前的工具栏。一般需要操作Ribbon上控件的时候,需要有一个全局对象变量来指向Ribbon对象。但是这个全局变量极不稳定,一旦有运行时错误发生或者代码重新编译后,则该全局将会清空。此时无法通过代码操作Ribbon上的控件了。
现在我们得知,在该全局变量中存放的实质是Ribbon对象的地址。Ribbon对象是在Excel工作簿打开的时候就创建的,只要工作簿存续期间,这个对象就一直存在。因而,我们只要在最开始记录这个对象的地址,当发生异常的时候将这个地址再次写入变量中即可。
这个想法是由MVP  Rory Archibald 提出的,并且在Mr Excel的社区中发了帖子。此外,在Ron de Bruin的个人网站中也有些例子,感兴趣的可以去看看。

在正式开始这个例子之前,我们需要学习一个API函数。千万别把API想得很神秘,就是一个函数而已。只不过在使用它之前我们必须声明一下。这次我们要使用的这个函数叫CopyMemory,利用它,可以将地址重新写回引用变量中。该API函数的声明语句为:
  1. Public Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
  2.     (ByRef destination As Any, ByRef source As Any, ByVal length As Long)

该函数接受3个参数:
destination:目标变量,即那个变量需要赋值
source:数据来源,即哪个数据需要赋值给变量
length:数据的长度。即第二个参数的数据的字节数。
可能有人会有疑问,为什么不能直接将地址写入变量中去呢?因为对象变量在栈中的数据是无法通过一般手段访问的,开发者总是访问到其所指向的变量。因而只能借助于API函数来实现。
9楼
amulee
问题重现
创建一个自定义Ribbon的工作簿,XML代码为:
  1. <customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="LoadRibbon">
  2.         <ribbon startFromScratch="true">
  3.                 <tabs>
  4.                         <tab id="customTab" label="Tools">
  5.                                 <group id="customGroup" label="HAHA">
  6.                                         <button id="Button1" getLabel="getLabel" imageMso="HappyFace" size="large" onAction="Bt_Click" />
  7.                                 </group>
  8.                         </tab>
  9.                 </tabs>
  10.         </ribbon>
  11. </customUI>


然后我们打开该文件,插入一个模块,添加以下代码:
  1. Dim IRibbon As IRibbonUI
  2. Dim lCount As Long

  3. 'Callback for customUI.onLoad
  4. Sub LoadRibbon(ribbon As IRibbonUI)
  5.     Set IRibbon = ribbon
  6. End Sub

  7. 'Callback for Button1 getLabel
  8. Sub getLabel(control As IRibbonControl, ByRef returnedVal)
  9.     returnedVal = CStr(lCount)
  10. End Sub

  11. 'Callback for Button1 onAction
  12. Sub Bt_Click(control As IRibbonControl)
  13.     lCount = lCount + 1
  14.     IRibbon.Invalidate
  15. End Sub


保存关闭后再次打开该工作簿,当我们点击功能区的那个笑脸后,可以发现数字逐渐增加。


 

目前看起来一切运行正常。现在我们在模块中再插入一个小程序如下:
  1. Sub Test()
  2.     MsgBox 1 / 0
  3. End Sub

写完之后不要运行该程序,我们再回到工作表中点击那个按钮。可以发现,此时程序无法正常运行并提示:


 

点击[调试]并为变量IRibbon添加一个监视,可以发现此时IRibbon已经为Nothing了。因为VBA项目重新编译了,所有的变量都清空了。


 

继续在工作表中添加一个按钮,将其指定宏设置为“Test”。
保存并关闭工作簿后重新开启。一开始,功能区上的按钮可以正常运行。然后我们点击之前在工作表中插入的按钮,可以发现程序出现错误。因为“Test”程序中除数为0。点击[结束]后,再次点击功能区的按钮,此时发现该按钮也无法正常运作。由于出现的运行时错误,所有的变量也清空了。
10楼
amulee
解决方案
为了解决该问题,我们需要在最开始记录下Ribbon功能区对象的地址。改写LoadRibbon过程如下:
  1. Sub LoadRibbon(ribbon As IRibbonUI)
  2.     Set IRibbon = ribbon
  3.     Sheet1.Range("B1").Value = ObjPtr(IRibbon)  '记录地址
  4. End Sub

以上过程会在加载功能区的时候将功能区对象的地址写入B1单元格。
再添加一个过程,将记录的地址再次恢复给变量,并添加一个按钮用于触发它。
  1. Sub GetRibbonBack()
  2.     Dim objRibbon As Object
  3.     Dim Ptr As LongPtr
  4.     Ptr = Sheet1.Range("B1").Value
  5.     '将地址复制回去
  6.     CopyMemory objRibbon, Ptr, LenB(Ptr)
  7.     Set IRibbon = objRibbon
  8.     Set objRibbon = Nothing
  9.     MsgBox "功能区已恢复"
  10. End Sub

保存并关闭工作簿后重新开启,可以发现B1单元个已经记录了功能区对象的地址。尝试点击“诱发错误的按钮”,出现错误对话框结束后,再次点击功能区的按钮,此时无法正常运行。点击“恢复Ribbon”按钮,此后功能区的按钮功能将恢复。


 
11楼
amulee
遗留问题
当我们声明一个全局变量或者静态变量时会发生什么情况呢?感兴趣的读者可以根据之前的演示进行一下测试。


附件下载:
VBA中数据存储的研究及其应用.rar

12楼
gfp12345678
灵活运用的真好
13楼
keven
感谢分享1!

免责声明

有感于原ExcelTip.Net留存知识的价值及部分知识具有的时间限定性因素, 经与ExcelTip.Net站长Apolloh商议并征得其同意, 现将原属ExcelTip.Net的知识帖采集资料于本站点进行展示, 供有需要的人士查询使用,也慰缅曾经的论坛时代。 所示各个帖子的原作者如对版权有异议, 可与本人沟通提出,或于本站点留言,我们会尽快处理。 在此,感谢ExcelTip.Net站长Apolloh的支持,感谢本站点所有人**绿色风(QQ:79664738)**的支持与奉献,特此鸣谢!
------本人网名**KevinChengCW(QQ:1210618015)**原ExcelTip.Net总版主之一

评论列表
sitemap