当前位置:文档之家› C#下的指针运用2014

C#下的指针运用2014

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

?向后兼容性。

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

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

任,否则就不能执行。在默认的代码访问安全策略中,只有代码运行在本地机器上,这才是可能的。如果代码必须运行在远程地点,例如Internet,用户就必须给代码授予额外的许可,代码才能工作。除非用户信任您和代码,否则他们不会授予这些许可。

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

________________________________________

1. 编写不安全的代码

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

[c-sharp] view plaincopy

1. unsafe int GetSomeNumber()

2. {

3. // code that can use pointers

4. }

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

[c-sharp] view plaincopy

1. unsafe class MyClass

2. {

3. // any method in this class can now use pointers

4. }

5. //同样,可以把成员标记为unsafe:

6. class MyClass

7. {

8. unsafe int *pX; // declaration of a pointer field in a class

9. }

10. //也可以把方法中的一个代码块标记为unsafe:

11. void MyMethod()

12. {

13. // code that doesn't use pointers

14. unsafe

15. {

16. // unsafe code that uses pointers here

17. }

18. // more 'safe' code that doesn't use pointers

19. }

20. //但要注意,不能把局部变量本身标记为unsafe:

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

csc /unsafe MySource.cs

或者

csc –unsafe MySource.cs

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

2. 指针的语法

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

[c-sharp] view plaincopy

1. int* pWidth, pHeight;

2. double* pResult;

3. byte*[] pFlags;

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

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

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

为寻址运算符。

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

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

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

[c-sharp] view plaincopy

1. int x = 10;

2. int* pX, pY;

3. pX = &x;

4. pY = pX;

5. *pY = 20;

6. //首先声明一个整数x,其值是10。接着声明两个整数指针pX 和pY。然后把pX 设置为指向x(换言之,把pX 的内容设置为x 的地址)。把pX 的值赋予pY,所以pY 也指向x。最后,在语句

*pY = 20 中,把值20 赋予pY 指向的地址。实际上是把x 的内容改为20,因为pY 指向x。注意在这里,变量pY 和x 之间没有任何关系。只是此时pY 碰巧指向存储x 的存储单元而已。

________________________________________

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

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

[c-sharp] view plaincopy

1. int x = 10;

2. int* pX, pY;

3. pX = &x;

4. pY = pX;

5. *pY = 20;

6. uint y = (uint)pX;

7. int* pD = (int*)y;

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

________________________________________

4. 指针类型之间的转换

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

[c-sharp] view plaincopy

1. byte aByte = 8;

2. byte* pByte= &aByte;

3. double* pDouble = (double*)pByte;

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

字节。

________________________________________

5. void 指针

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

[c-sharp] view plaincopy

1. int* pointerToInt;

2. void* pointerToVoid;

3. pointerToVoid = (void*)pointerToInt;

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

________________________________________

6. sizeof 运算符

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

[c-sharp] view plaincopy

1. int x = sizeof(double); //这将设置x 的值为8。

2. //使用sizeof 的优点是不必在代码中硬编码数据类型的大小,使代码的移植性更强。

3. sizeof(sbyte) = 1; sizeof(byte) = 1;

4. sizeof(short) = 2; sizeof(ushort) = 2;

5. sizeof(int) = 4; sizeof(uint) = 4;

6. sizeof(long) = 8; sizeof(ulong) = 8;

7. sizeof(char) = 2; sizeof(float) = 4;

8. sizeof(double) = 8; sizeof(bool) = 1;

9. //也可以对自己定义的结构使用sizeof,但此时得到的结果取决于结构中的字段。不能对类使用sizeof。它只能用于不安全的代码块。

________________________________________

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

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

[c-sharp] view plaincopy

1. struct MyStruct

2. {

3. public long X;

4. public float F;

5. }

6. //就可以给它定义一个指针:

7. MyStruct* pStruct;

8. //对其进行初始化:

9. MyStruct Struct = new MyStruct();

10. pStruct = &Struct;

11. //也可以通过指针访问结构的成员值:

12. (*pStruct).X = 4;

13. (*pStruct).F = 3.4f;

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

使用这个指针成员访问运算符,上述代码可以重写为:

[c-sharp] view plaincopy

1. pStruct–>X = 4;

2. pStruct–>F =

3.4f;

3. //也可以直接把合适类型的指针设置为指向结构中的一个字段:

4. long* pL = &(Struct.X);

5. float* pF = &(Struct.F);

6. //或者

7. long* pL = &(pStruct–>X);

8. float* pF = &(pStruct–>F);

________________________________________

9. 类成员指针

前面说过,不能创建指向类的指针,这是因为垃圾收集器不维护指针的任何信息,只维护所引

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

[c-sharp] view plaincopy

1. class MyClass

2. {

3. public long X;

4. public float F;

5. }

6. //然后就可以为它的字段X 和F 创建指针了,方法与前面一样。但这么做会生成一个编译错误:

7. MyClass myObject = new MyClass();

8. long* pL = &( myObject.X); // wrong--compilation error

9. float* pF = &( myObject.F); // wrong--compilation error

X 和F 都是非托管类型,它们嵌入在一个对象中,存储在堆上。在垃圾收集的过程中,垃圾收集器会把MyObject移动到内存的一个新单元上,这样,pL 和pF 就会指向错误的存储单元。由于

存在这个问题,所以编译器不允许以这种方式把托管类型的成员地址分配给指针。

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

[c-sharp] view plaincopy

1. MyClass myObject = new MyClass();

2. fixed (long* pObject = &( myObject.X))

3. {

4. // do something

5. }

6. //在关键字fixed 后面的圆括号中,定义和初始化指针变量。这个指针变量(在本例中是pObject)的作用域是花括号标记的fixed 块。这样,垃圾收集器就知道,在执行fixed 块中的代码时,不能移动MyObject 对象。如果要声明多个这样的指针,可以在同一个代码块前放置多个fixed 语句:

7. MyClass myObject = new MyClass();

8. fixed (long* pX = &( myObject.X))

9. fixed (float* pF = &( myObject.F))

10. {

11. // do something

12. }

13. //如果要在不同的阶段固定几个指针,还可以嵌套整个fixed 块:

14. MyClass myObject = new MyClass();

15. fixed (long* pX = &( myObject.X))

16. {

17. // do something with pX

18. fixed (float* pF = &( myObject.F))

19. {

20. // do something else with pF

21. }

22. }

23. //也可以在同一个fixed 语句中初始化多个变量,但这些变量的类型必须相同:

24. MyClass myObject = new MyClass();

25. MyClass myObject2 = new MyClass();

26. fixed (long* pX = &( myObject.X), pX2 = &( myObject2.X))

27. {

28. // etc.

29. }

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

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

[c-sharp] view plaincopy

1. using System;

2. namespace Wrox.ProCSharp.Memory

3. {

4. class MainEntryPoint

5. {

6. static unsafe void Main()

7. {

8. int x=10;

9. short y =–1;

10. byte y2 = 4;

11. double z = 1.5;

12. int* pX = &x;

13. short* pY = &y;

14. double* pZ = &z;

15. Console.WriteLine(

16. "Address of x is 0x{0:X}, size is {1}, value is {2}",

17. (uint)&x, sizeof(int), x);

18. Console.WriteLine(

19. "Address of y is 0x{0:X}, size is {1}, value is {2}",

20. (uint)&y, sizeof(short), y);

21. Console.WriteLine(

相关主题
文本预览
相关文档 最新文档