科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道基础软件可恶的VB 快乐的.NET

可恶的VB 快乐的.NET

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

由于Windows操作系统的系统功能大部分是以API函数的形式提供给用户,而API函数的最初编写基本上都是以C语言实现的

作者:莫艺潜 来源:天极网 2007年11月4日

关键字: VB

  • 评论
  • 分享微博
  • 分享邮件
由于Windows操作系统的系统功能大部分是以API函数的形式提供给用户,而API函数的最初编写基本上都是以C语言实现的,由此一来,C语言的指针操作遍布Windows编程的方方面面,以前的VB程序员低人一等原因很简单:VB所有的版本(不包括VB.net)都不支持完整的指针操作,所以根本无法调用一些Wincows的核心功能!微软既不能完全将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} dim b as int32=70   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,b,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编译器:结构的元素在内存中按其出现的顺序排列
<StructLayout(LayoutKind.Sequential)> _
Public Structure DEFUDT_Test
 Public bytb As Byte
 Public i32a As Int32
End Structure

Public Function fnGetIntptr1() As IntPtr
 ’取得一个4字节数组指针
 Dim tabytTest(3) As Byte
 ’以下语句告诉net垃圾回收进程不对tabytTest进行处理,也就是说tabytTest占用的内存区域固定不变。
 Dim thObject As GCHandle = GCHandle.Alloc(tabytTest, GCHandleType.Pinned)
 Dim tpObject As IntPtr = thObject.AddrOfPinnedObject() ’取得指向字节数组的指针

 ’取得一个指向32位内存数据的指针,
 ’由于使用gchandle取指针的方法只能对引用的对象有效,
 ’所以对如int32等值类型必须使用将其封装成为一个对象的方法以变为引用类型
 Dim ti32Test As Object = Convert.ToInt32(0)
 ’以下语句告诉net垃圾回收进程不对ti32test进行处理,也就是说ti32Test的内存位置固定不变。
 Dim thObject1 As GCHandle = GCHandle.Alloc(ti32Test, GCHandleType.Pinned)
 Dim tpObject1 As IntPtr = thObject1.AddrOfPinnedObject() ’取得ti32Test的首地址

 Dim tudtTest1 As DEFUDT_Test
 ’由于结构是一种值类型变量,为保证指针申请方便,我们申请
 ’取得一个和结构tudtTest1大小一致的字节数组指针,只要空间占用长度和结构一样就可以了
 ’由于net在结构封装中会插入额外的数据位,所以一定要用sizeof方法得到结构在非托管使用时的实际大小
 Dim tudtTest(Marshal.SizeOf(tudtTest1)) As Byte
 Dim thObject2 As GCHandle = GCHandle.Alloc(tudtTest, GCHandleType.Pinned)
 Dim tpObject2 As IntPtr = thObject2.AddrOfPinnedObject() ’取得指向结构的指针

 ’在这儿你可以写对指针处理的任意代码(在例2中会给予补充)……

 ’在使用完毕后一定要释放指针指向的内存块,让垃圾回收器可对这个内存块回收处理
 If thObject.IsAllocated Then
  thObject.Free()
 End If
 If thObject1.IsAllocated Then
  thObject1.Free()
 End If
 If thObject2.IsAllocated Then
  thObject2.Free()
 End If
End Function

  上例中指针流程处理可以归纳为:

  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, 0, Convert.ToInt32(77)) ’向ti32Test变量指向的地址写入32位整数77
MsgBox("现在ti32Test的值为:" & ti32Test) ’因为变量存储地址的数据已改为77,所以显示为77
’以下这句之所以可行,因为ti32Test是32位整数,而tpObject指向的tabytTest数组刚好有4个元素
’而每一个byte元素都占用8位,合起来就是32位,和ti32Test占用的空间一样。这就印证了前面提’
’到的net中指针没有指向类型的说明。
Marshal.WriteInt32(tpObject, 0, ti32Test)

’以下代码再将tabytTest字节数组的内容理解为一个int32整数,
’并将值赋值给tudtTest结构中的int32元素

’我们使用Marshal.OffsetOf(GetType(DEFUDT_Test), "i32a").ToInt32以取得i32a元素在结构中的内存偏移位置
’所以New IntPtr(tpObject2.ToInt32 + Marshal.OffsetOf(GetType(DEFUDT_Test), "i32a").ToInt32)就临时产生了
’一个指针并指向i32a所在的内存地址(, 这个方法也说明了指针可以以字节为单位进行加减计算以指向合适的变量。
’Marshal.ReadInt32的作用是从指针中读取一个32整数。
Marshal.WriteInt32(New IntPtr(tpObject2.ToInt32 + Marshal.OffsetOf(GetType(DEFUDT_Test), "i32a").ToInt32), _
0, Marshal.ReadInt32(tpObject))
’这儿可以将字节数组的内容复制到真正的结构中
MsgBox(Marshal.OffsetOf(tudtTest1.GetType, "i32a").ToInt32)
tudtTest1 = CType(Marshal.PtrToStructure(tpObject2, GetType(DEFUDT_Test)), DEFUDT_Test)

MsgBox("结构tidtTest1中i32a元素的值为:" & tudtTest1.i32a) ’此处将显示刚赋的值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)
  ’必要时可以使用重载策略支持任意类型的数组
 Dim thObject As GCHandle = GCHandle.Alloc(ni_aobjArraySource, GCHandleType.Pinned)
 Dim tpObject As IntPtr = thObject.AddrOfPinnedObject() ’取得目标数组的指针
 Marshal.Copy(tpObject, ni_aobjArrayDest, 0, ni_aobjArrayDest.Length() - 1)

End Sub

’调用语句为:

Dim tachrSource() As Char = {"a", "b", "c"}
Dim tabytDest(5) As Byte ’2个byte和1个char占用的空间相等

’以下代码你可以设个断点,然后用快速监视看看tabytDest中的数据内容
sbArrayCopy(tachrSource, tabytDest) ’将tachrSource数组内容复制到tabytDest数组中,注意了类型完全不同哦

  结束语:

  本来想演示几个必须用指针调用的API的,但由于那几个函数的调用都要很多结构,考虑到文章的篇幅,就留待日后吧……。在net中,我个人认为熟练使用marshal下的函数就已经可以将指针玩得团团转了!不过有一点必须注意,我们使用指针只是为了兼容并访问一些常规方法不能实现的功能函数,而不是提倡使用指针!老实说,在VB.net中使用指针是否可以提高性能我也没测试过。话又说回来,指针是一个较复杂的问题,初学编程总会被指针指得晕头转向,但学好了编程水平绝对有大幅提高。我水平有限,可能本文内容有很多错漏之处,希望大家不吝指正(感觉怎么好象说这话倒象卖书的)。有什么问题可以与我讨论一下,你认为C#有什么比VB.net强也不妨告诉我,我的信箱是 missilecat@163.com QQ:85403578,期待你的来信。

  后记

  这几个月来我做梦都守候VB2005的出现,微软承诺的运算符重载,中断再继续的调试功能……,期待VB.net功能可以全面压倒C#,然后我的薪金水涨船高……

查看本文来源

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章