篆体字网 > 知识库

copymemory

来源:篆体字网 2024-01-05 18:17:30 作者:篆字君

第三节 经典错误代码集锦

好的,现在我们可以来看看VB妈妈好心没做好事的几个例子了。先说明一下,以下所有这些例子都来自这个帖子热心朋友的回复,它们都共享第一节里声明的模块级变量和常数。

3.1 我在0楼的代码——结果为何变短

Sub test5() String1 = STR_E String2 = String$(7, 0) CopyMemory pString1, ByVal VarPtr(String1), 4 CopyMemory ByVal String2, ByVal pString1, 14 Debug.Print String2 '得到的不是PowerVB,而是“P o w e”? End Sub

这个例子的运行过程如下:

(1)第1个CopyMemory得到的是String1的地址,并把这个地址作为源地址传给第2个CopyMemory
(2)第2个CopyMemory从String1的地址拷贝14个字节。由于VB中字符串的内部表示是Unicode,所以这时得到的14个字节的内容是“P-\0-o-\0-w-\0-e-\0-r-\0-V-\0-B-\0-”(注意,其中的“-”是我加入用来分割字符的,并不真的包括在字符串内存中)。
(3)由于CopyMemory的第一个参数是ByVal String2,是一个字符串,而VB会自动对API函数中的字符串参数做UA转换。所以,系统会把14个字节的Unicode空字符串String2转为7个字节的ANSI空字符串,并存在一个临时变量中,假设叫_tmp。
(4)然后系统把拷来的14字节数据“P-\0-o-\0-w-\0-e-\0-r-\0-V-\0-B-\0-”向_tmp拷。注意_tmp只有7字节,所以这里有溢出的危险。
(5)由于_tmp只有7字节,所以_tmp实际只得到头7个字节的数据,就是“P-\0-o-\0-w-\0-e-”
(6)最后VB要把ANSI字串再转回Unicode字串,并把转回的结果赋给String2。AU转换就是是将英文的 1 个字节扩张为 2个字节,这样String2最终的内容是“P-\0-\0-\0-o-\0-\0-\0-w-\0-\0-\0-e-\0-”,用Debug打印出来,可不就是“P o w e”么?

下面这个表总结了上述过程:

另外,我还做了下图来说明上面的过程:

哎,这个图画得太杰出了。一目了然啊!你看,第②步的Unicode字符串硬是被当做ANSI字串在第③步被强行AU了,所以结果“浮肿”了很多(被插入好些空格)。

这个例子如何改对呢?中间两句不改,前后各改/增加一句,如下:

String2=String$(LenB(String1), 0) '先确保_tmp2长度足够
CopyMemory pString1, ByVal VarPtr(String1), 4
CopyMemory ByVal String2, ByVal pString1, LenB(String1)
String2=StrConv(String2, vbFromUnicode) '再做UA转换以抵消VB多做的一次AU转换

3.2 阿勇在11楼的代码——结果为何变胖

'阿勇11楼 Sub test8_Yong() Dim pString1 As Long String1 = STR_E String2 = String$(14, 0) CopyMemory pString1, VarPtr(String1), 4 CopyMemory String2, ByVal pString1, 4 Debug.Print String2, StrConv(String2, vbFromUnicode) End Sub

运行上面的程序,你会发现string2的结果也是浮肿的。

(0)第1个CopyMemory获得String1变量指针,并存在pString1里;

(1)第2个CopyMemory首先从pString1里把String1缓冲区地址拷出来;然后把这个地址拷到临时字符串变量_tmp2里,也就是让_tmp2的字符串缓冲区指针指向String1的字符串缓冲区地址;

(2)之后VB把_tmp2的内容做AU转换,并把AU转换的内容拷到一个新分配的字符串缓冲区中,并把String2的字符串缓冲区指针指向这个新分配的地址。

看下面的图,第②步之后_tmp2字符串缓冲区里是来自String1的Unicode字符串,可是它还是在第③步被VB妈妈当初ANSI字符串强行AU,然后再倒手给String2,String2里得到的字符串可不就是浮肿的么:)

这个例子如何改对呢?最后加个String2=StrConv(String2, vbFromUnicode)把VB多做的那一次AU转换抵消就可以了。

3.2.1 插播:字符串内存的初始化

Q:String2=String$(14, 0)这一句可以不要么?

A:这一句是必要的,相当于给它初始申请内存。千万别用没分配内存的指针,否则很容易崩溃的。另外,String$(7,0)相当于加入7个Chr(0)字符(vbNullChar),Space$(7)则相当于加入7个Chr(20)字符(空格)。由于VB字符串允许含Null字符,所以这两种初始化方法都可以的。

3.3 Modest在16楼的方法——错误的代码正确的结果

这个方法Modest自己起初的评语是“通俗易懂、还不出错”,打眼一望,我也很赞同这个评语。之后有些细节感觉想不通,请求继续解释。赵老虎分析完之后说“根本就是瞎猫碰上死老鼠”,“实际上是在胡乱操作内存”。刚看到这个评语,我觉得Tiger_Zhao这厮也太那个了,好歹老魏也是VB版一大虾啊。可是看完他的解释之后,我却发现这个评语还真中肯。这里要赞一下Modest,当真好气度,换了别人这么说我,甭管对错,我得先一蹦老高。闲话少说,咱们来看老魏的代码吧。核心的代码如下:

CopyMemory pString1, String1, 4CopyMemory pString2, String2, 4CopyMemory pString2, pString1, LenB (String1)

初看起来,这个代码貌似是分别得到两个字符串缓冲区的指针,然后把String1的字符串缓冲区拷给String2,结果也是正确的。当真“通俗易懂、还不出错”。但是细想想,你会发现这一切都不大靠谱:

(1)首先对于头2个CopyMemory而言,既然VB妈妈会做UA转换,那么pString1和pString2得到的应该分别是_tmp1和_tmp2的地址,而这两个临时变量在CopyMemory调用之后会被释放掉,也就是说pString1和pString2得到的其实是无效的地址(见下图)。

所以这两个语句应该改成这样;

CopyMemory pString1, VarPtr(String1), 4CopyMemory pString2, VarPtr(String2), 4

(2)其次,就算我们把头两个语句改成上面这样,第3个CopyMemory真的在拷贝字符串缓冲区么?看出来了么?要拷贝字符串缓冲区,第3个语句应该加上ByVal,像这样:

CopyMemory ByVal pString2, ByVal pString1, LenB (String1)

(3)可是,诡异的是,就这么一段漏洞百出的代码,它的运行结果明明是正确的啊?这是为什么呢?看下面Tiger_Zhao的解释。

Sub test9_Modest() Dim String1 As String Dim String2 As String Dim pString1 As Long Dim pString2 As Long '这 4 个变量每个长 4 字节,在栈上按地址从低到高为: '| pString2 | pString1 | String2 | String1 | String1 = "PowerVB" String2 = Space$(Len(String1)) 'String1、String2 分别指向两个字符串 CopyMemory pString1, String1, 4 CopyMemory pString2, String2, 4 'pString1 、pString2 值为已被释放的临时变量的地址,无所谓 CopyMemory pString2, pString1, LenB(String1) '由于不加 ByVal,其实是从变量 pString1 的地址向变量 pString2 的地址复制 14 个字节 '等于直接操作栈内存,让变量向左复制(看后面插播的“覆盖模式”),于是 '| pString2 : 原 pString1 的值 '| pString1 : 原 String2 的字符串指针 '| String2 : 原 String1 的字符串指针 '| String1 : 不确定 | Debug.Print String2 End Sub

3.3.1 插播1:关于栈

(1)变量都是存放在中的。这个内存由编译器自动分配释放。
(2)对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。所以先声明的变量的内存地址会比后声明的高。比如上面的示例里那样。

3.3.2 插播2:CopyMemory自动处理覆盖

A: CopyMemory是Copy还是Cut?原来地址中内存的内容还在么?

Q: 复制。源和目标内存不交叉,源不变;如果交叉,还会自动处理覆盖情况。

A: “自动处理覆盖情况”是什么意思?是说:万一交叉了,还会把被覆盖的复原,而且把目的地址挪开点么?

Q: 看下面的例子:

'如果有一个数组 a() : 00-01-02 'a) CopyMemory a(1), a(0), 2 '结果是 00-00-01,会避免:先用 a(0) 覆盖 a(1),然后再用 a(1) 覆盖 a(2),最终变成 00-00-00 'b) CopyMemory a(0), a(1), 2 '结果是 01-02-02,会避免:先用 a(2) 覆盖 a(1),然后再用 a(1) 覆盖 a(0),最终变成 02-02-

3.3.3 插播3:VB挂掉的话会有什么后果?

Modest这段代码如果把变量的次序换一下,VB就可能会挂掉。大家可以试一试:P

Q: 这种崩溃有可能波及到整个操作系统么?还是甭管我怎么瞎折腾,把VB关掉就万事大吉?

A: 不可能波及整个系统。因为VB之所以崩溃,就是因为系统搞的鬼。操作系统为了保护自己,会强行关闭它自认为潜在的“威胁”,所以会把VB搞掉。而一旦保护不了,而且出错,就会出现所谓的“蓝屏”。常规情况的话,重开VB就可以了。

Q:在调试状态下用错指针,会导致VB崩溃。那如果是编译成可执行程序,里面有错误的指针操作的话,可能会蓝屏么?

A:通常有进程保护,不会蓝屏。除非你的程序操作了系统级资源。

上一篇:2011式保安员服装

下一篇:2012年诺贝尔经济学奖

相关阅读