版权声明:可以任意转载,转载时请务必以超链接形式标明如下文章原始出处和作者信息及本声明
作者:xixi
出处:http://blog.csdn.net/slowgrace/archive/2009/04/26/4124347.aspx
本文主要来自此帖的讨论,感谢unsigned、Soyokaze、Chen8013、Tiger_Zhao、m60a1、myjian、PctGL等朋友的指点,本文很多文字直接来自他们的发言。
在VB中指针的算法非常诡异,你必须计算元素的大小,因为VB不会帮你完成这项工作。
1、引言:VC指针和VB指针(来自unsigned)
指针只是一个形象的说法,实际在内存当中就是一个地址。VB没有C语言的指针类型,从而带来的问题是,在VB中,对你声明的长整形指针变量自加自减只是机械地加了“1”而已,并没有像C编译器那样,会根据指针指向的数据类型自动地加减这个数据类型的字节数。比如:
int *p = a;
p ++;
a的每个元素是两个字节,p自加后,指向a[1]。因此,如果是在VB里,想指向a(1),需要这样:
a( 0) = 99
a( 1) = 88
Dim p As Long
p = VarPtr( a( 0))
p = p + 2
再如C/C++等当中,当定义好一个结构指针类型之后,进行指移偏移运算的时候,会以结构大小进行偏移移位:
unsigned char x;
unsigned char y;
short reserved;
int z;
} MyStruct , * LPMyStruct;
LPMyStruct P;
P = 0;
P ++; //这里是P = (LPMyStruct)((unsigned long)P + sizeof(MyStruct)),而不是P = (LPMyStruct)((unsigned long)P + 1);
而在VB当中则完全需要自己对指针偏移进行正确计算,这种计算可以借助于API,以及VarPtr、StrPtr以及address of等等操作符做到。不过在这之前,我们最好还是熟悉一下VB使用内存的方式。
2、VB使用内存的方式
2.1 按字长对齐(来自unsigned)
主流编译器为了内存访问的优化,会按机器字长进行内存对齐,从而导致某些结构例如:
unsigned char x;
unsigned char y;
int z;
} MyStruct;
看上去只有 sizeof(unsigned char) + sizeof(unsigned char) + sizeof(int) = 1 + 1 + 4。但是在Win32平台下,在没有强制做存储压缩的情况下,编译器却实际上让它占用了8字节的空间。x和y各占一个字节,所以x和y被分到同一个机器字长的空间当中;由于z是int,刚好是一个机器字长,为了访问z的时候能够一次性读入cache,而不是先从第一个机器字长的空间当中读取半个z,然后再读取第二个机器字长的空间才读取到后半个z,编译器就把z直接放到了下一个机器字长的空间当中,从而就导致了y和z中间隐藏了2个字节的空间。
而在有的编译器当中,数据是压缩存储的,也就是说
x as byte
y as byte
z as long
End Type
这样一个结构,在有的编译器当中就是6个字节。从而如果你在调用一个API的时候,刚好碰到上面的C结构,那么你就很有可能会把一个在C当中占了8字节的结构定义成了只有6个字节。如此你在调用API的时候,就会导致内存访问溢出,后果是非常严重的。
2.2 堆和栈
通常,局部变量是放在栈中的,动态分配的内存则是放在堆中的。如果你声明一个含动态数组成员的结构,那么结构中指向数组注册地址的指针是在栈中的,4字节。而数组的具体内容则是在堆中的,一般是连续存放的。详见堆与栈的区别(1)和堆和栈的区别(2)。栈里的内存的对齐方式,通常由编译器决定;堆里内存的对齐方式,通常由操作系统决定。
注意1:数组元素的存放应该是连续的,只是首址按双字对齐。也就是,数组的第一个元素的地址是双字对齐的,后续元素则不再按双字对齐,而是依次连续不间断地排放,中间没有填充空闲字节。既使是结构数组也是这样(未经xixi验证)。
注意2:VB6中动态数组和字符串是动态分配内存的。也就是变长字符串运算和redim动态数组是要重新分配内存的,重新Redim时,各元素地址会发生变化。
2.3 有关无符号长整型
此外,在VB中运用指针,我们还必须处理缺乏无符号长整型数据类型的问题,详见这篇文章。
下面让我们通过几个例子来体会VB中指针位移的计算:
3、一个关于局部变量的内存分配的例子(来自Chen8013)
Dim t As Boolean , strTest$ , bB As Byte , iLong& , iT% , iKK&
strTest = @H_404_351@"123456"
Debug . Print Hex$( VarPtr( t))
Debug . Print Hex$( VarPtr( strTest )), Hex$( StrPtr( strTest))
Debug . Print Hex$( VarPtr(bB))
Debug . Print Hex$( VarPtr( iLong))
Debug . Print Hex$( VarPtr( iT))
Debug . Print Hex$( VarPtr( iKK))
strTest = strTest & @H_404_351@"ABCD"
Debug . Print Hex$( VarPtr( strTest )), Hex$( StrPtr( strTest))
End Sub
输出:
12F442 (t 的地址,分配12F440~12F443,使用12F442~12F443)
12F43C 190484 (strTest 的地址,分配12F43C~12F43F,全用。字符串内容存放190484起的地方)
12F43A (bB 的地址,分配12F438~12F43B,使用12F43A)
12F434 (iLong 的地址,分配12F434~12F437,全使用)
12F432 (iT 的地址,分配12F430~12F433,使用12F432~12F433)
12F42C (iKK 的地址,分配12F42C~12F42F,全使用)
12F43C 1900FC (strTest实际内容的地址发生了变化,可见变长字符串运算是要重新分配内存的)
这几个变量都是局部变量,分配在栈中(栈的内存地址是从高向低生长的)。它在分配的时候,要进行边界对齐,32位系统中这些简单变量都是分配的4字节空间,占用空间的尾数都是0~3、4~7、8~B、C~F。对于占用不到4字节的变量,多余的空间都是空出不用的。
问:为何bB不是使用12F43B而是使用12F43A?不是高字节对齐么?
答:这我也不太清楚了,应该跟硬件有很大的关系。现在的数据总线是32位的,内存←→cpu 之间的数据交换,每次肯定是4个字节。它选择在第三个字节上,可能在运算、分离数据上实现起来方便些吧。
4、几个关于结构的例子
4.1.例1(来自Soyokaze)
见这篇文章。
4.2.例2(来自Soyokaze)
a as Byte
b as Integer
c as Long
d as Byte
End Type
按理说,访问成员d,从结构首地址算起,应该偏移7个字节。然而这是错的!!因为VB会按32位对齐分配结构,a--1Byte,b--2Bytes,还差1Byte,留空(当然不能把c截断啦),所以c安排到了首地址开始偏移32位的位置。后面依此类推。这样,访问成员d,需要偏移8字节。还有,涉及到结构中包含数组时,也要遵循这个原则。当然数组元素的存放应该是连续的,只是首址按双字对齐。
4.3.例3(来自unsigned)
4.4. 例4(来自unsigned)
不过要注意的是,在VC中,对于没有任何一元素有必要进行对齐处理的结构,其占用宽度不会被调整。
5、会用到字节计算的情形(来自myjian、unsigned和eglic)
会用到字节计算的情形通常有以下几种:
(1)只要涉及了内存操作,都需要计算字节....
(2)需要按字节来判断结构版本的API通常都在输入的结构里有一个cbSize成员,在使用之前把当前结构的字节值设置进去,API就根据这个值决定向传入的这个指向某结构的指针写入什么样的内容(实际上传入给API的是一个指针,只是VB允许直接用byref xxxx as type这样的方式方便代码编写,好看明白)。
(3)定义与API结构对应的VB结构的时候,如果自己翻译也要注意计算好字节(不过一般的结构在APIVIEWER里也有了,不用自己整)。常见的C与VB的类型对应有如下几种:
C -> VB
char -> byte
short -> integer
int -> long
(4)用Unicode版API,对于string型参数,要转化为long,并用StrPrt传递字符串缓冲区指针进去。对于结构里面,固定长度的字符串,则可能会需要用byte array(对于unicode应该可以使用[string * N]),当然也可以使用两倍的Byte Array或者Integer Array然后Copy进去。无论是哪种方式吧,这时候你都算仔细结构所占的字节。
(5)数据序列化的时候(见第6节和第7节的两个例子)
6、实例1:来自kathyxin16的这个帖子
Private Declare Sub CopyMemory Lib @H_404_351@"kernel32.dll" Alias @H_404_351@"RtlMoveMemory" ( ByRef Destination As Any , ByRef Source As Any , ByVal Length As Long)
Private Enum eCommand
vTAB
tPOS
vGET
sSND
vPHT
sTAB
sPHT
End Enum
Private Type tPack
eCMD As eCommand 'long型,4个字节
iID As Integer '高位对齐,4字节
bFilepack() As Byte '4字节,变长数组的注册地址
End Type
Private Sub Command1_Click()
Dim number As Integer , i As Integer
Dim data1() As Byte
Dim data2() As Byte
Dim pack1 As tPack
pack1 . eCMD = tPOS
pack1 . iID = 1
ReDim pack1 . bFilepack( 0)
ReDim data2( 0)
pack1 . bFilepack( 0) = 2
number = LenB( pack1) '12字节
ReDim data1( number - 5) '最后的指针不需要
CopyMemory data1( 0 ), pack1 , number - 4
CopyMemory data2( 0 ), pack1 . bFilepack( 0 ), UBound( pack1 . bFilepack) + 1
Debug . Print @H_404_351@"data2(0): " & data2( 0)
Debug . Print @H_404_351@"eCMD: " & data1( 0)
Debug . Print @H_404_351@"iID: " & data1( 4)
End Sub
在这个例子中,Kathy要在计算机之间传输指令,tPack类型传不了,只能以字节数组形式传递,所以要把数据拷到字节数组byte(0)里。这也就是所谓的“数据序列化”问题:把数据序列化进行传输,对方收到后再还原。
Kathy声明的结构共12字节。第一个字长存的是eCMD;第二个字长存的是iID(高位对齐);第三个字长是指针,存的是动态数组的注册地址(和动态数组的地址不是一回事)。上面的代码的意思是分两次拷:先拷头、再拷动态数组。这里的关键在于,要准确找到要拷贝的pack1的所有内容的内存地址:
(1)先把emcd和iId拷过去(这个应该是在栈上)。这个地址好搞,就是pack1,拷贝长度是8字节;
(2)再用另外一个copymemory语句来拷动态数组的实际内存(应该是在堆上)。这个动态数组的地址,用vpack1.bFilepack(0)传进去,拷贝的长度取为ubound(vpack1.bFilepack)+1。
注意1:LenB()和Len()的区别。这里如果用Len(pack1)得到的是10。Len()返回“理论上”的长度,LenB()返回实际占的字节数。
注意2:在拆解data时要小心,iID有可能占data1(4)和data1(5)2位,因为它是Integer类型、双字节。
7、实例2:来自m60a1的这个帖子。(代码来自Chen8013)
Private Declare Sub CopyMemory Lib @H_404_351@"kernel32" Alias @H_404_351@"RtlMoveMemory" ( Destination As Any , Source As Any , ByVal Length As Long)
Private Type sockData
bteFileData() As Byte
strReqirt As String
End Type
Sub Test()
'客户端
Dim sendData As sockData
Dim byte_s() As Byte
'---------------
Dim bToSend() As Byte
Dim i& , m&
sendData . strReqirt = @H_404_351@"ABCDE"
ReDim sendData . bteFileData( 79) '假设有80字节数据要发送
For i = 0 To 79
sendData . bteFileData( i) = i + i
Next
'计算 sendData ‘具体内容’所占空间
i = ( UBound( sendData . bteFileData) + 1) * 1
m = LenB( sendData . strReqirt)
'* 1 是:字节型变量每个元素占1字节
ReDim bToSend( i + m + 7) 'bToSend(m + 8 - 1) 另外附加8个字节把结构信息保存下来
'必需放在开头
CopyMemory bToSend( 0 ), i , 4
CopyMemory bToSend( 4 ), m , 4
CopyMemory bToSend( 8 ), sendData . bteFileData( 0 ), i
CopyMemory bToSend( 8 + i ), ByVal StrPtr( sendData . strReqirt ), m
Call ReceiveData( bToSend)
End Sub
Sub ReceiveData( bDataBuf() As Byte)
'用这个过程来模拟 Winsock 接收数据后的处理
Dim recData As sockData
Dim bLen& , sLen& , i&
CopyMemory bLen , bDataBuf( 0 ), 4
CopyMemory sLen , bDataBuf( 4 ), 4
'下面这两句必须进行后才能 Copy 数据
ReDim recData . bteFileData( bLen)
recData . strReqirt = String( sLen / 2 , Chr$( 0))
CopyMemory recData . bteFileData( 0 ), bDataBuf( 8 ), bLen
CopyMemory ByVal StrPtr( recData . strReqirt ), bDataBuf( 8 + bLen ), sLen
For i = 0 To bLen
Debug . Print i , recData . bteFileData( i)
Next
Debug . Print recData . strReqirt
End Sub
这个例子也是有关数据序列化的。m60a1需要从客户端发送数据给服务器端,要发送的数据中有一部份是字节数组,一部份是字符串。需要服务器端正确的区分这两种种数据并储存起来。一开始,她想这样发:
sock.sendData "这里是字符串" & "/" & 这里是字节数组
但是对于上面的包,她在服务器端怎么拆也拆不正确的数据出来。于是,她决定在客户端构建一个结构体,将上面二种数据分别存入结构体中,然后把这个结构体的内存内容,直接复制给字节数组,再把数组发送给服务器端,最后在服务器端再将结构体还原出来。这是个典型的数据序列化问题。
上面这段代码(据说这是Chen8013在VB崩溃4、5次之后调通的:)的意思是,分别拷贝字符串和字节数组,并在头里分别记住字符串和字节数组的大小。值得一提的是,其中的语句:
recData.strReqirt = String(sLen / 2,Chr$(0))
表示让VB给.strReqirt分配正确大小的贮存空间,并且用ASCII 码为0的字符填充。注意,这里sLen 的值是字符串所占的字节数,而String函数需要的是字数,所以需要除以2。这里sLen / 2 表示整数除法。如果写成sLen / 2,它将进行浮点运算,且是双精度的,传参数时,又要转换成整数。所以这里用sLen / 2运行效率比 sLen / 2 高n多n多.......当然,如果不是反复的存在这种运算,也可以不考虑它。
8、避免字长对齐问题导致内存溢出的办法(来自unsigned)
1).在定义结构的时候注意字节对齐,尽可能对定义的结构当中的“空洞”进行填充,例如定义成:
unsigned char x;
unsigned char y;
short reserved;
int z;
} MyStruct;
2).使用第三方接口时,比如API,应该在使用时尽量分析清楚,以定义出一个完全匹配的结构
x as byte
y as byte
reserved as integer
z as long
End Type
9、小结
(1)VB中使用指针,需自行计算字节数。在数据序列化、内存操作、API调用时常会需要用到这种技术。
(2)VB6中无论是局部变量还是结构变量都是32位对齐的。
(3)VB6中动态数组、字符串等使用动态内存,首字对齐、连续存放。
(4)LenB、sLen / 2