版权声明:可以任意转载,转载时请务必以超链接形式标明如下文章原始出处和作者信息及本声明
作者:xixi
出处:http://blog.csdn.net/slowgrace/archive/2009/09/14/4550116.aspx
本文来自此帖的冗长讨论,感谢Tiger_Zhao的全程指点和陈辉、阿勇、马云剑等很多朋友的热心参与。本文其他部分在:(一)、(三)、(四)。
第三节 经典错误代码集锦
好的,现在我们可以来看看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, 4
CopyMemory pString2, String2, 4
CopyMemory pString2, pString1, LenB (String1)
初看起来,这个代码貌似是分别得到两个字符串缓冲区的指针,然后把String1的字符串缓冲区拷给String2,结果也是正确的。当真“通俗易懂、还不出错”。但是细想想,你会发现这一切都不大靠谱:
(1)首先对于头2个CopyMemory而言,既然VB妈妈会做UA转换,那么pString1和pString2得到的应该分别是_tmp1和_tmp2的地址,而这两个临时变量在CopyMemory调用之后会被释放掉,也就是说pString1和pString2得到的其实是无效的地址(见下图)。
所以这两个语句应该改成这样;
CopyMemory pString1, VarPtr(String1), 4
CopyMemory 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:通常有进程保护,不会蓝屏。除非你的程序操作了系统级资源。
WEI WAN DAI XU