由于Windows操作系统的系统功能大部分是以API函数的形式提供给用户,而API函数的最初编写基本上都是以C语言实现的,由此一来,C语言的指针操作遍布Windows编程的方方面面,以前的VB程序员低人一等原因很简单:VB所有的版本(不包括VB.net)都不支持完整的指针操作,所以根本无法调用一些Windows的核心功能!微软既不能完全将API实现的功能封装到控件库或类库中,又不允许VB程序员使用指针以调用!所以当时很多用VC可以实现的程序VB程序员就实现不了并不是水平问题,
而是微软的产品方针在作怪。那年头VB唯一的优势就是可视化界面编程,以大量的控件实现一拖即用的效果,但时至今日,C#的可视化编程和VB差不多没什么两样,界面编写的优势一去不复返,根据VB6留下来的没法进行底层处理的烂名声,一大批的VB程序员转投C#阵营。综上所述,要完全发挥windows的能量,首先必须掌握指针这一武器,我们VB程序员需要指针这把利剑,以刺穿老板的心脏,让他们以与C#程序员同等的待遇对待我们。
一、.net指针的定义
1、内存地址
由于可能看本文的VB读者或许没有接触过指针的概念,所我在此对指针的概念定义简单作一阐述。
计算机的数据处理是基于内存地址进行的,所谓内存地址就是将内存可以存储数据的空间人为地按字节顺序进行数字编号,当程序对某一变量进行存取时计算机就根据变量的地址以确定变量中实际存储的数据内容。表1 是一个假想的内存地址示意图,我们可以看到,假设计算机将字节数组a实际分配到2000开始的连续三个地址空间后,对数组的赋值就会在这三个字节的内存空间中进行。另一个变量b由于是一个32位的整数,所以它占用了2003、2004两个地址的空间,在3070中放了一个byte类型的c变量
表1:假想的内存地址示意图
内存地址 |
0 |
1 |
…… |
2000 |
2001 |
2002 |
2003 |
2004 |
…… |
3070 |
…… |
最高编号取决于内存的容量 |
变量实际数据 |
系统保留 |
系统保留 |
|
a |
b |
c |
70 |
0 |
|
|
|
|
变量声明 |
|
|
|
dim a(2) as byte={a,b,c} |
@H_181_404@
|
dim c as byte |
|
|
2、指针的定义和作用
1)指针是一特殊的变量,它存储的就是一个内存地址编号,当某一个地址编号存储在指针变量中时,我们称指针指向以这个地址开始的数据。由于现在使用的win98/win2000/winxp/2003等操作系统均为32位系统,所以都是采用一个32位的整数记录内存地址(可表示的范围就是0~2的31次方),所以以上系统的指针存储的实际数据都是一个32位的整数。(但以后的64位Windows系统会以一个64位的整数记录内存地址,到时指针存储的将是一个64位的整数)。
指针到底有什么作用呢,在汇编语言及C语言的黑暗时代(很久很久以前的事了……),使用指针存储一个地址,然后根据这个地址进行数据的存储是获得高效率程序的最好途径,(一来,计算机内部是直接使用地址作数据存储的,计算机并不明白在高级语言中定义如A,B这样的变量名;二来地址的传送很方便,如函数的参数是以堆栈的方式传送的,以一个32或16位(Dos系统是16位的)的整数表示的数组地址入栈处理比将整个数组的内存入栈高效得多),但现在我们并不需要使用这种底级的程序优化方法,我们学习旨针的宗旨应该是:因为API是用C编写的,所以我们必须找到一个兼容使用这些函数的途径。
2)在net中的指针是没有指向类型之分的,所谓指向类型就是指针地址代表的实际数据类型(这是针对C语言而言的,在C语言中,如果一个指针的指向类型为char型,那么这个指针的指向的数据就是指针指向的地址值及其后一连续编号的地址所代表的内存区间中储存的16数据)。也就是说,net指针存储的是一个地址,而这个地址表示的数据类型可以理解为以这个地址开始的内存空间所能表示的各类数据。如表1中,假设我们使用一个储存着地址2000的指针p,我们可以理解p指向一个字节类型,其值就是a;也可以理解p指向一个3位字节的数组,其值是一个连续的a,c。
二、指针的使用
在本文的以下部分,我们假定程序已包括以下语句以引用net中处理指针的命名空间:
Imports System.Runtime.InteropServices
1、指针的申请
在net中,指针就是一个intptr的变量,由于指针存储的是一个32位整数地址的事实,我们可以直接将一个内存范围内的整数赋值给一个指针(使用intptr的构造函数,然后起直接将内存地址作为参数传入,例:dim pa as new intptr(777) ’这样就定义了一个指针,他指向内存地址以777开始的区域),然后就可以使用指针对这个地址中储存的数据进行读取或写入操作。但这种做法是危险的,地址分配是系统自动进行的,我们没法假定某一时刻在某一内存位置储存着什么数据,好象上面777编号的地址中存储了什么?这是系统的保留区域,你如想向这个地址读写数据,程序出错那是铁定的事了!除非你真正知道那个地址中到底放有什么数据,否则绝不要这样做。
正确的做法:首先向系统申请一块足够大的内存空间,然后保存这个内存空间的首地址到一个指针中。由于前面介绍过,net指针没有数据类型之分,所以假若我们想申请有某一指向类型的指针,只要申请一个这个数据类型有同等内存占用长度的字节数组或VB.net变量,然后将这个数组或变量的首地址存储在指针中就算申请成功了。表2是一些内存空间占用长度相等的windows标准数据类型和Vb.net数据类型的对照表
表2:windows标准数据类型和Vb.net数据类型的对照表
windows标准数据类型 |
vb.net数据类型 |
应该申请的字节数组 |
空间占用长度(以字节为单位) |
Byte |
Byte |
|
1 |
CHAR |
Byte |
|
1 |
WChar |
Char |
Dim byt(1) as byte |
2 |
Short |
Int16 |
Dim byt(1) as byte |
2 |
INT/INT32/LONG/UINT |
Int32 |
Dim byt(3) as byte |
4 |
DWORD |
Int32 |
Dim byt(3) as byte |
4 |
Bool |
Int32 |
Dim byt(3) as byte |
4 |
INT64/LONG64/ULONG64 |
Int64 |
Dim byt(7) as byte |
8 |
例1:以下程序我们申请几个指向不同类型的指针:
’使用<StructLayout(LayoutKind.Sequential)>属性告诉net编译器:结构的元素在内存中按其出现的顺序排列 |
上例中指针流程处理可以归纳为:
1、 定义一个具有合适内存长度的引用变量(关于引用变量和值变量的差异可以参观vb.net的书籍)
2、使用GCHandle.Alloc方法将变量的内存区域固定下来。
3、使用GCHandle对象的AddrOfPinnedObject取得该内存区域的首地址并赋值给指针变量.
4、对指针进行操作
5、使用GCHandle对象的free方法释放指针指向的内存区域以便net垃圾回收器可以回收这个内存空间
2、指针所指向数据的存取
在.net中,对指针指向数据的存储函数都封装在marshal类中,主要的函数包括:Copy、PtrToStringUni 、PtrToStructure 、OffsetOf、WriteXXX,RreadXXX等,其中WriteXXX的表示向指针所表示的地址中写入XXX类型的数据,而ReadXXX中作用就是将指针所在地址的数据以XXX类型方式读出。看例程2,我们使用这些方法演示对例1那几个指向不同类型数据的指针作数据存/取操作。
例2:演示向例1申请得到的几个指针执行写入及读取数据的操作.
Marshal.WriteInt32(tpObject1,Convert.ToInt32(77)) ’向ti32Test变量指向的地址写入32位整数77 |
三、指针的疯狂应用
大伙应该已对指针的应用有了初步了解,最后我将已一个指针的简单例程所为本文的结束,这是我在论坛请教C#和VB.net的区别时,一个头上有两颗星星的大侠给出的一段他认为VB.net不具备指针功能而无法实现的C#函数,作用是将一个数组的内容以指针的方式复制到另一个数组之中(其实我个人认为这与语言有否指针功能无关,net数组对象已很好地实现了copy功能,不妨参考帮助中数组的成员方法)。但本文只是为了说明VB.net与C#享有平等权利,所以我也用指针实现一个同等功能的数组复制函数(这种复制的确比较灵活),以演示将一个char类型的数组内容存储到一个byte类型的数组中。
例3:
Public Overloads Sub sbArrayCopy(ByRef ni_aobjArraySource() As Char,ByRef ni_aobjArrayDest() As Byte) |