C#下的指针运用

来源:互联网 发布:sql server 按月分组 编辑:程序博客网 时间:2024/05/17 08:11

 

 

    使用指针,就可以访问实际内存地址,执行新类型的操作。例如,给地址加上4字节,就可以查看甚至修改存储在新地址中的数据。下面是使用指针的两个主要原因:

 

  • 向后兼容性。

      尽管.NET 运行库提供了许多工具,但仍可以调用内部的Windows API函数。 对于某些操作来说,这可能是完成任务的唯一方式。这些API 函数都是用C语言编写的,通常要求把指针作为其参数。但在许多情况下,还可以使用DllImport声明,以避免使用指针,例如使用System.IntPtr 类。

  • 性能。

      在一些情况下,速度是最重要的,而指针可以提供最优性能。假定用户知道自己在做什么,就可以确保以最高效的方式访问或处理数据。但是,注意在代码的其他区域中,不使用指针,也可以对性能做必要的改进。请使用代码配置文件,查找代码中的瓶颈,代码配置文件随VS2005 一起安装。但是,这种低级内存访问也是有代价的。使用指针的语法比引用类型更复杂。而且,指针使用起来比较困难,需要非常高的编程技巧和很强的能力,仔细考虑代码所完成的逻辑操作,才能成功地使用指针。如果不仔细,使用指针很容易在程序中引入微妙的难以查找的错误。例如很容易重写其他变量,导致堆栈溢出,访问某些没有存储变量的内存区域,甚至重写.NET 运行库所需要的代码信息,因而使程序崩溃。另外,如果使用指针,就必须为代码获取代码访问安全机制的高级别信任,否则就不能执行。在默认的代码访问安全策略中,只有代码运行在本地机器上,这才是可能的。如果代码必须运行在远程地点,例如Internet,用户就必须给代码授予额外的许可,代码才能工作。除非用户信任您和代码,否则他们不会授予这些许可。

 

 

     这里强烈建议不要使用指针,因为如果使用指针,代码不仅难以编写和调试,而且无法通过CLR 的内存类型安全检查。下面就开始介绍指针的使用。

 

 


 

    1. 编写不安全的代码


    因为使用指针会带来相关的风险,所以C#只允许在特别标记的代码块中使用指针。标记代码所用的关键字是unsafe。下面的代码把一个方法标记为unsafe

 

    任何方法都可以标记为unsafe—— 无论该方法是否应用了其他修饰符(例如,静态方法、虚拟方法等)。在这种方法中,unsafe 修饰符还会应用到方法的参数上,允许把指针用作参数。还可以把整个类或结构标记为unsafe,表示所有的成员都是不安全的:

 

 

 

     如果要使用不安全的局部变量,就需要在不安全的方法或语句块中声明和使用它。在使用指针前还有一步要完成。C#编译器会拒绝不安全的代码,除非告诉编译器代码包含不安全的代码块。标记所用的关键字是unsafe。因此,要编译包含不安全代码块的文件MySource.cs(假定没有其他编译器选项),就要使用下述命令:

csc /unsafe MySource.cs

     或者

csc –unsafe MySource.cs

     注意: 如果使用 Visual Studio 2005,就可以在项目属性窗口中找到编译不安全代码的选项。

 

 


 

 

 

2. 指针的语法

 

    把代码块标记为 unsafe 后,就可以使用下面的语法声明指针:

 

 

 

    这段代码声明了4 个变量,pWidth pHeight 是整数指针,pResult double 型指针,pFlags byte 型的指针数组。我们常常在指针变量名的前面使用前缀p 来表示这些变量是指针。在变量声明中,符号*表示声明一个指针,换言之,就是存储特定类型的变量的地址。

 

    声明了指针类型的变量后,就可以用与一般变量的方式使用它们,但首先需要学习另外两个运算符:

 

  • “&” 表示“取地址”,并把一个值数据类型转换为指针,例如int 转换为*int。这个运算符称为寻址运算符。

     

  • “*” 表示“获取地址的内容”,把一个指针转换为值数据类型(例如,*float 转换为float)。这个运算符称为“间接寻址运算符”(有时称为“取消引用运算符”)

     

    从这些定义中可以看出,&*的作用是相反的。

 

    下面的代码说明了如何使用这些运算符:

 

 

 

 

 

 


 

3. 将指针转换为整数类型

 

    由于指针实际上存储了一个表示地址的整数,所以任何指针中的地址都可以转换为任何整数类型。指针到整数类型的转换必须是显式指定的,隐式的转换是不允许的。例如,编写下面的代码是合法的:

 

    可以把一个指针转换为任何整数类型,但是,因为在32 位系统上,地址占用4 字节,把指针转换为不是uintlong ulong 的数据类型,肯定会导致溢出错误(int 也可能导致这个问题,因为它的取值范围是–20 亿~20 亿,而地址的取值范围是0~40 亿)C#是用于64 位处理器的,地址占用8 字节。因此在这样的系统上,把指针转换为非ulong 的类型,就可能导致溢出错误。还要注意,checked 关键字不能用于涉及指针的转换。对于这种转换,即使在设置checked 的情况下,发生溢出时也不会抛出异常。.NET 运行库假定,如果使用指针,就知道自己要做什么,并希望出现溢出。

 

 


 

 

4. 指针类型之间的转换

 

    也可以在指向不同类型的指针之间进行显式的转换。例如:

 

 

    这是一段合法的代码,但如果要执行这段代码,就要小心了。在上面的示例中,如果要查找指针pDouble 指向的double,就会查找包含1 字节(aByte)的内存,并和一些其他内存合并在一起,把它当作包含一个double 的内存区域来对待—— 这不会得到一个有意义的值。但是,可以在类型之间转换,实现类型的统一,或者把指针转换为其他类型,例如把指针转换为sbyte,检查内存的单个字节。

 

 


 

5. void 指针

 

    如果要使用一个指针,但不希望指定它指向的数据类型,就可以把指针声明为void

 

    void 型指针的主要用途是调用需要void*型参数的API 函数。在C#语言中,使用void指针的情况并不是很多。特殊情况下,如果试图使用*运算符间接引用void 指针,编译器就会标记一个错误。

 


 

 

6. sizeof 运算符

 

    这一节将介绍如何确定各种数据类型的大小。如果需要在代码中使用类型的大小,就可以使用sizeof 运算符,它的参数是数据类型的名称,返回该类型占用的字节数。例如:

 

 

 

 


 

7. 结构指针:指针成员访问运算符

 

    结构指针的工作方式与预定义值类型的指针的工作方式是一样的。但是这有一个条件:结构不能包含任何引用类型,这是因为前面介绍的一个限制—— 指针不能指向任何引用类型。为了避免这种情况,如果创建一个指针,它指向包含引用类型的结构,编译器就会标记一个错误。假定定义了如下结构:

 

 

    但是,这个语法有点复杂。因此,C#定义了另一个运算符,用一种比较简单的语法,通过指针访问结构的成员,该语法称为指针成员访问运算符,其符号是一个短划线,后跟一个大于号:–>使用这个指针成员访问运算符,上述代码可以重写为:

 


 

9. 类成员指针

 

    前面说过,不能创建指向类的指针,这是因为垃圾收集器不维护指针的任何信息,只维护所引用的信息,因此创建指向类的指针会使垃圾收集器不能正常工作。但是,大多数类都包含值类型的成员,可以为这些值类型成员创建指针,但这需要一种特殊的语法。例如,假定把上面示例中的结构重写为类:

 

    X F 都是非托管类型,它们嵌入在一个对象中,存储在堆上。在垃圾收集的过程中,垃圾收集器会把MyObject 移动到内存的一个新单元上,这样,pL pF 就会指向错误的存储单元。由于存在这个问题,所以编译器不允许以这种方式把托管类型的成员地址分配给指针。

    解决这个问题的方法是使用fixed 关键字,它会告诉垃圾收集器,类实例的某些成员有指向它们的指针,所以这些实例不能移动。如果要声明一个指针,使用fixed 的语法如下所示:

 

 

    在上述情况中,是否声明不同的指针,让它们指向相同或不同对象中的字段,或者指向不与类实例相关的静态字段,这一点是不重要的。

    下面给出一个使用指针的示例:PointerPlayaround。它执行一些简单的指针操作,显示结果,还允许查看内存中发生的情况,并确定变量存储在什么地方:

 

 

 

    这段代码声明了4 个值变量:

● int x

● short y

● byte y2

● double z

    还声明了指向这3 个值的指针:pXpYpZ然后显示这3 个变量的值,以及它们的大小和地址。注意在获取pX, pY pZ 的地址时,我们查看的是指针的指针,即值的地址的地址!还要注意,与显示地址的常见方式一致,在Console.WriteLine()命令中使用{0:X}格式说明符,确保该内存地址以十六进制格式显示。最后,使用指针pX x 的值改为20,执行一些指针转换,如果把x 的内容当作double类型,就会得到无意义的结果。