版权声明:可以任意转载,转载时请务必以超链接形式标明如下文章原始出处和作者信息及本声明
作者:xixi
出处:http://blog.csdn.net/slowgrace/archive/2009/04/27/4130448.aspx
本文来自这个帖子的讨论,感谢Tiger_Zhao、Soyokaze、unsigned、Chen8013等的指点。
API函数中常常需要用到指针(内存地址)参数,这通常对应为C中的unsigned int类型。unsigned int为32位无符号整数,用于表示逻辑内存可达4G[0,232-1]。在VB中没有无符号整数这种类型,通常会把指针声明为long类型,这是一种32位有符号整数,取值范围是[2-31,231-1]。这种差别,有时会导致在VB6中计算指针位移的时候溢出或得到错误的计算结果,因此,有必要采用特别的算法来实现无符号整形的加减法。
1、几种不同的溢出
1.1. Long和Unsigned在坐标轴上的"4G"关系
Long和Unsigned都是32位整数,因此它们的十六进制表示“范围”是基本一致的,都是从0到&HFFFF FFFF。所不同的是,在VB中,负整数是以补码表示的,符号位在最高位(0表示正数,1表示负数)。因而,在VB中大于7FFF FFFF的十六进制数实际上都被理解为负数,包括从&H8000 0000到&HFFFF FFFF的数。因而,long型和unsigned int的取值范围在坐标上的位置错了231个单位长度,如下图:
其中,unsigned从231-1到232-1区间内的数和Long从-231到0区间内的数的十六进制表示完全一样,但对应的十进制数值差232,对于32位地址空间来说,可以认为它们等价,即(“==”表示“等价于”):
[&H80000000,&HFFFFFFFF]long==[-231,-1]10进制
[&H80000000,&HFFFFFFFF]unsigned==[231,232-1]10进制
我们不妨称这种现象为4G取模等价性或简称4G等价性。为便于讨论,我们先定义几种不同的溢出:
1.2. unsigned溢出
如果一个数大于4G(以下我们用4G代表2^32,2G代表2^31)或者小于0,对于Unsigned类型就是溢出了,这个我们不妨称为超越unsigned边界,或称unsigned溢出。考虑两个指针的运算,由于unsigned只能表达4G的逻辑内存,当指针运算结果出现unsigned溢出时,根据不同的编程需求,我们通常会对函数有两种期待。一种是,希望负责运算的函数报告溢出、以防止异常的内存操作,不妨称这种处理方法叫直接溢出(不妨简称溢出法);另一种是希望它取模不溢出(不妨简称取模法),也就是:
a)如果两个无符号整数i和j的和sum超过4G,那么我们希望这时或者希望这个结果指针指到sum mod 4G的位置,也就是sum-4G的位置。假设i>j,就有:
i+j==i+j-4G==i+(j-4G)==i+与j对应的有符号负整数
也即,我们希望指针算法,在计算两个和超过4G的指针加法时,能够把它转换为减法(1个正数与1个负数的加法),而不是溢出。
b)同样,如果两个无符号整数i和j的差sub小于0,我们也希望这个结果指针指到sub mod 4G的位置,也就是sub+4G的位置。假设i<j,就有:
i-j==i-j+4G==i-(j-4G)==i+与j对应的有符号负整数
也即,我们希望指针算法,在计算两个差小于0的指针减法时,能够把它转换为加法(1个正数与1个负数的加法),而不是溢出。
只要一个指针算法能对所有的unsigned溢出都能始终如一地坚持其中一种处理方法(直接溢出或取模不溢出),我们就可以认为它是一个正确的算法。
1.3. long溢出
如果一个数小于-2G,或者大于2G-1,对于long类型就是溢出了,我们不妨称这个数字long越界,这种溢出为long溢出。因为本文主要讨论在VB中实现指针运算,所以long溢出才是我们要集中精力对付的溢出。值得注意的是,由于负整数的补码表示,当我们以十六进制表示的大于&H7FFFFFFF的大正数送到VB程序里时,并不会溢出,而只是被理解为一个负整数。只有当你以十进制来表示一个long越界的数字并赋给long型变量时,或计算结果long越界时,才会产生long溢出。
1.4. VB6中指针运算时可能出现的溢出
考虑在VB中进行指针位移的各种情况。假设有两个无符号整数uintA,uintB,考虑VB中如下代码的可能后果
Dim uintA As Long,uintB As Long Dim lngSub As Long,lngSum As Long lngSub = uintA - uintB lngSum = uintA + uintB |
a)uintA和uintB位于[0,2G)
a.1)lngSub和lngSum均位于[0,2G)。运算过程不会产生溢出,并且结果正确
a.2)lngSub位于[-2G,0)。这时long不溢出,但是unsigned溢出。从上面的讨论可知,如果期望对unsigned溢出采用取模法处理,则期望的正确结果是lngSub+4G。前面讨论过lngSub+4G的十六进制表示和lngSub的十六进制表示是完全一致的,所以这时运算过程不会产生long溢出,并且结果正确;但是如果期望对unsigned溢出采用溢出法处理,则这时long不溢出就是不正确的,需要特别设计算法处理。
a.3)lngSum位于[2G,4G)。这时long溢出。需要特别设计算法来处理。
b)uintA和uintB中一个位于[0,2G),一个位于[2G,4G)。
b.1)考虑lngSum的计算。假设uintB位于[2G,4G),这时uintB被VB理解为负整数,即按此式运算:uintA + uintB - 4G,运算结果的实际取值区间是[-2G,2G),所以不会long溢出。
b.1.1)lngSum位于[2G,4G)。这时,所期望的指针运算结果uintA + uintB和实际的运算结果uintA + uintB - 4G差4G,它们在4G的表示空间里实际上是一个数,因此运算结果也正确。
b.1.2)lngSum位于[4G,6G)。这时unsigned溢出。取模法期望的指针运算结果是lngSum-4G,正好和VB实际的运算结果相同;但如果希望用溢出法处理unsigned溢出,则这里需要特别的算法设计。
b.2)考虑lngSub的计算。
b.2.1)如果uintA位于[2G,4G),被VB理解为负数,VB运算结果是uintA-4G-uintB,其实际取值区间是[-4G,0],有可能long溢出,需要特别设计算法处理。
b.2.2)如果uintB位于[2G,4G),被VB理解为负数,VB运算结果是uintA-(uintB-4G),其实际取值区间是[0,4G],有可能long溢出,需要特别设计算法处理。
c) uintA和uintB都位于[2G,4G)。这时uintA和uintB都被VB当作负数处理。
c.1) lngSum= (uintA-4G) + (uintB-4G),取值区间是[-4G,0],有可能long溢出。
c.2) lngSub=(uintA-4G) - (uintB-4G)= uintA - uintB,取值区间[-2G,2G),不会溢出,运算结果正确。
由上面完备的讨论可以看出,如果期望对unsigned溢出采用取模法处理(事实上本文后续两个算法都采用的是取模法),那么只要运算过程不long溢出,最后的运算结果都能正确反映我们对指针运算的期望。排除unsigned溢出,观察以上讨论结果,可以发现对无符号整数做加法(注意,我们这里暂先只讨论加法)主要有两种溢出情况:
(1)uintA和uintB位于[0,2G),lngSum位于[2G,4G)。比如uintA= &H7FFF FFFF,uintB= 1
(2)uintA和uintB都位于[2G,4G),lngSum位于[-4G,-2G)。比如uintA = &H8000 0000,uintB = &H6FFFF FFFF
从VB的观点去描述这2种情况其实也很简单,就是两个正数相加越界或者两个负数相加越界。或者说,指针加法运算中的long溢出主要是由于参与运算的同符号数的和的绝对值超过2G造成的。
2、一个简单但有缺陷的无符号整数加法算法
Function UnsignedAdd (ByVal Start As Long,ByVal Incr As Long) As Long Const SignBit As Long = &H80000000 UnsignedAdd=(Start Xor SignBit) + Incr Xor SignBit End Function |
上面的函数中,变量Start可以理解为是指针的起始位置(它永远是个无符号正数,虽然超过&H7FFFFFFF时会被VB认为是负数),Incr可以理解位是希望指针做的位移(它可负可正,但通常绝对值不大)。
2.1. xor
这个函数的核心语句用到了XOR运算符。异或运算符是两个操作数不相同时取真(1),否则为假(0)。详见MSDN里的说明。要注意的是,这个运算符的优先级是低于“+”的,所以,上面函数里的核心语句实际上等价于
UnsignedAdd=((Start Xor SignBit) + Incr) Xor SignBit
另外,异或运算符有两个特点:
a)数a两次异或同一个数b(a=a^b^b)仍然为原值a。
b)使特定位的值取反 (mask中特定位置1,其它位为0 s=s^mask)
2.2. 与&H80000000异或
现在我们来讨论上面这个函数的常数SignBit。&H80000000就是十六进制数80000000,变成二进制数就是1000 0000 0000 0000 0000 0000 0000 0000。由于上面提到的异或运算符的取反的特性,任何整数和它异或都是相当于其它位没变,只把最高位(MSB: Most Significant Byte)变了,也就是把负数变成了正数、把正数变成了负数。所以,在上面这个函数中,把这个数称为SignBit。
2.3. 函数结果的等价性
(Start Xor SignBit) + Incr确保运算结果的后31位正确,MSB是错的;这个结果之后再次与SignBit异或,把MSB反过来,MSB得到了正确值。所以,如果这个函数在运算过程中不溢出,计算结果应该是与期望的计算结果等价。
2.4. 函数过程对溢出的处理(感谢Soyokaze耐心指点)
由第一节的分析知,指针加法运算中的long溢出主要是由于参与运算的同符号数的和的绝对值超过2G造成的。这个函数正好可以处理这两种溢出。因为当Start和Incr符号相同时,这个函数把start符号反转后与incr相加,从而相对于一正一负两个数相加,因而不会出现上面两种溢出。
2.5. 这个函数会引入新的溢出么
人算不如天算。这个算法虽然用异或符号位的方法来规避同符号数相加越界,可是这时,符号相反的数在函数运算过程中被变成了相同符号的数,从而人为地引入了新的“同符号数相加”,从而也引入了新的溢出。对这种溢出,Soyokaze做出如下的解释:
a)若Start>0,Incr <0,反转后相加时,只要Start绝对值大于Incr绝对值,就不会溢出。也正符合指针没有负地址的要求。
b)或Start<0,Incr>0,反转后相加时,同样只要Start绝对值大于Incr绝对值,就不会溢出。否则两者相加其实是超过4G。
也就是说,以上两种新溢出实际上对应于unsigned溢出。也就是,这个函数貌似设计的是对unsigned溢出采用直接溢出法处理。不过这种处理方式是否始终如一呢?很显然是不如一的,因为在2.4节我们看到,对可能的unsigned溢出,该函数通过两次异或符号位的方法来处理,实际上对unsigned溢出采用了取模不溢出法。所以说,这是个前后言行不一的、无法自圆其说的、有缺陷的算法。
3、一个比较完备的算法
据Tiger_Zhao说,《高级 Visual Basic》中提供的 UAdd() 是比较正确的。或者用下面这个(感谢Tiger_Zhao逐字逐句做了注释):
'注:以下注释中的加减运算都是无符号运算 Private Function UnsignedAdd(ByVal lX As Long,ByVal lY As Long) As Long Dim lX4 As Long Dim lY4 As Long Dim lX8 As Long Dim lY8 As Long Dim lResult As Long '提取最高位' lX8 = lX And &H80000000 lY8 = lY And &H80000000 '提起次高位' lX4 = lX And &H40000000 lY4 = lY And &H40000000 '剩余位直接相加' lResult = (lX And &H3FFFFFFF) + (lY And &H3FFFFFFF) If lX4 And lY4 Then '次高位同时为 1,和为 &H80000000 'lResult = lResult + 次高位的和(即&H80000000) + X的最高位 + Y的最高位 '其中向更高位 &H100000000 的进位直接忽略了 lResult = lResult Xor &H80000000 Xor lX8 Xor lY8 ElseIf lX4 Or lY4 Then '次高位只有一个 1' 'lResult = lResult + 次高位的和(即&H40000000) + X的最高位 + Y的最高位' If lResult And &H40000000 Then '剩余位的和向次高位有进位 ' lResult + 次高位的和(即&H40000000) '= lResult + &H80000000 - &H40000000 '= lResult Xor &HC0000000 lResult = lResult Xor &HC0000000 Xor lX8 Xor lY8 Else lResult = lResult Xor &H40000000 Xor lX8 Xor lY8 End If Else '次高位全0,和为0' 'lResult = lResult + 次高位和(即&H00000000) + X的最高位 + Y的最高位' lResult = lResult Xor lX8 Xor lY8 End If UnsignedAdd = lResult End Function |
我们的目标是用 UnsingedAdd() 进行无符号数的加法,其值域范围就是 UInt32 : &H00000000 - &HFFFFFFFF [0 - 4294967295)。而 VB 的 Long 是 Int32: &H80000000 - &H7FFFFFFF [(-2147483648) - 2147483647),可以用与 Unsigned 运算的只有 &H00000000 - &H7FFFFFFF (0 - 2147483647)。那么其最高位 &H80000000 (2147483648) 就不能参与用 VB.Long 的加法运算,必须自己处理,这就是上面这个函数中要提取 lX8、lY8 的原因。另外,次高位 &H40000000 (1073741824) 的相加会向最高位产生进位,在 VB.Long 的加法运算中就是溢出错误,也必须自己处理,因此这个函数中也提取 lX4、lY4。至于剩余的位(对 lX或lY 进行 And &H3FFFFFFF 操作后)的加法,最多进位到次高位 &H40000000,完全可以用 VB.Long 进行加法运算。
而所谓的自己处理,就是用 Xor 来模拟不进位的二进制加法防止溢出错误,其实就是在已知某个位的值时,通过 Xor 强制反转该位的值(0/1反转)。(这一点其实和第二节的算法的思路是一致的)。
注意,这个函数中除了用了前面提到过的Xor运算符的计算特点外,还用了AND运算符的如下特点:
a. 清零特定位 (mask中特定位置0,其它位为1,s=s&mask)
b. 取某数中指定位 (mask中特定位置1,其它位为0,s=s&mask)
3.3. 函数结果的等价性
上面这个计算过程的实质就是两串01互相按位在做加法,结果完全正确。另外,由于自动处理可能的进位,这个函数实际上是按第一节所说的“取模不溢出”的方法来处理unsigned溢出的。
3.4. 函数过程对溢出的处理
上面这个计算过程把所有可能出现溢出的情况(表现在最高位和次高位的计算)都自行处理了,杜绝了一切可能的溢出。
3.5. 这个函数会引入新的溢出么
NO,理由同上。
3.6. 这个函数能应对无符号整数的减法么
考虑两个无符号整数uintA和uintB,我们的问题是,UnsignedAdd(uintA,-uintB)的输出能等价于uintA-uintB么?答案是:能!
首先,考虑4G内存空间的取模等价性,我们有:
uintA-uintB==uintA-uintB+4G==uintA+(4G-uintB)
所以我们有
UnsignedAdd(uintA,-uintB)==UnsignedAdd(uintA,4G-uintB)==uintA+(4G-uintB)==uintA-uintB
上式第一个和第三个等价于是由4G内存空间的取模等价性而来的,第二个等价于是由前面我们已说明的函结果的等价性而来的。也就是说,这个函数完全可以应对无符号整数的减法。
不过,也许我们会有个疑问:当我们输入一个位于[2G,4G)的uintB,那么这个函数的结果是把这个uintB当作大正数处理来做加法,还是当作负数来做减法呢?再想一下,其实这不是个问题,因为无论是按哪种理解,得到的计算结果都完全一致。这里面的根本原因就在于4G内存空间的取模等价性,以及这个算法是采取“取模不溢出”来处理unsgined溢出的。
4、后记:鸟和鱼——来自WallesCai
花了很多天功夫来理解这个指针算法(当然,其中也有很多时间是因为太困难而逃避去网上晃荡过去了),终于勉强达到一个自己满意的理解程度了。其间得到很多网友长篇累牍、无限耐心的指点(可惜比较笨的我开窍起来着实很慢),可能也因为此,相应的讨论帖在CSDN总社区被置顶。在要结束这个话题之前,看到WallesCai在这个帖子118楼的发言,感觉写得很好,抄来做结束语吧:)
这就像鸟和鱼一样。鸟是在空气中飞行的,它习惯于空中的生活,鱼是在水里"飞行"的,它习惯于水中的生活。
鸟可以尝试潜水,体验一下鱼的生活片断,鱼也可以跃出水面,享受一下鸟的乐趣。但是毕竟那不是属于它们自己的生活。
SO,可以做一条有性格的鱼,也可以做一只有性格的鸟,但是你无法真正的改变生存的环境。
学了C,觉得没有指针是很难想像的事。学了VB,觉得没有指针是很正常的事。
所以真的要体会指针,就去学C吧,用鸟的语言去表达鱼的生活,或者用鱼的语言去表达鸟的生活,都是吃力又不讨好的事。
这里不去讨论各种语言的优劣高低,那太无聊。但是学通了一种语言再去学其他的语言,会有不同的体会,这是肯定的。SO,在VB毕业之前,了解一下有指针这个东西就行,不必花太多力气深究了,我相信99。99%的VB程序用不到这个。楼上那些和你"讨论"VB指针技术的家伙,都是通了VB又懂C的,所以楼主还是先自己努力提高吧。