非安全代码
我们知道,.NET通用语言运行环境为C#引入了一种托管的安全编程方式,指针存取、变量地址计算、对象销毁等等操作在托管编程环境下都是C#所不允许的,这大大改进了传统C/C++的安全性。但事物往往是多面性的,在摈弃指针等内存的直接存取方式的同时,也丧失了它在某些问题上的便利性,比如某些和操作系统底层的交互、内存映射设备的存取等等。在某些特殊的任务上,我们甚至不希望引入自动垃圾收集这种“不确定的系统消耗”,C#通过引入非安全(unsafe)代码来迎合这些特殊需要。
在非安全代码中,C#允许直接操作指针、获取变量地址、进行指针类型与整数类型之间的转换等在C/C++中经常出现的操作。非安全代码通过关键字“unsafe”来标识。编译非安全代码时必须加上编译选项“/unsafe”,否则编译报错。“unsafe”关键字可以加于类型(类、结构、接口、委派)声明、成员(构造器、析构器、域、方法、属性、事件、索引器、操作符)声明,以及用大括号“{}”括起来的语句块。标识了unsafe的代码块又称为unsafe上下文。看下面的例子:
unsafe struct Point//unsafe类型
{ public int *x,y;……}
class Test
public static unsafe void Swap(ref int *x,ref int *y)
//unsafe成员指针类型和托管环境下的引用类型有点相似,数据包含在它们指向的内存区块。但指针类型和托管环境的引用类型有着本质的区别——自动垃圾收集器不追踪指针指向的数据区块,实际上自动垃圾收集器根本不知道指针及其数据的存在!
指针类型包含其指向的内存块的数据类型,这个数据类型在C#中被限制为“非托管类型”和“void类型”,“非托管类型”不能是引用类型。实际上,指针类型指向的内存区块根本不能是引用类型,也不能是包含引用类型的自定义结构。但引用类型可以包含指针类型,引用类型会被自动垃圾收集器管理,而声明为指针指向的“非托管类型”不被自动垃圾收集器管理!
指针在传递参数的时候,也可以用out和ref来表达传址(传引用),从而可以在函数内改变指针变量本身(也就是变量的地址)。和C/C++中的传址方式一样,如果在这样的函数中将指针的值改变为函数内局部变量的地址,在退出函数时,由于系统往往会回收这些地址空间,程序便会发生异常。看下面的例子:
using System;
class Test{
unsafe static void F(out int* pi1, ref int* pi2) {
int i=100;
int j=200;
pi1 = &i;
pi2 = &j;
Console.WriteLine((int)pi1+“,”+(int)pi2);
}
unsafe static void Main() {
int i = 10;
int* px1 = &i;
int* px2 = &i;
Console.WriteLine((int)px1+“,”+(int)px2);
F(out px1, ref px2);
Console.WriteLine((int)px1+“,”+(int)px2);
Console.WriteLine(“*px1 = {0}, *px2 = {1}”,*px1, *px2);
}
}
程序输出:
1243332,1243332
1243304,1243308
1243304,1243308
*px1 = 13249636, *px2 = 13253284
注意前三行的结果依赖于特定的执行环境,最后一行结果则属于未定义的程序行为。可以看到,函数F内的变量i、 j的存储空间在退出函数栈后被回收,指针变量px1、px2指向的数值将不确定。鉴于此,我们一般不要用out修饰指针类型参数,只有在确定改变的地址空间在退出函数后不会被回收,才可以使用ref来修饰指针类型参数。
指针类型可以和8种C#的整数类型之间进行转换,这种转换必须用括号“()”的形式进行明晰转化。空类型“null”也可以作为指针类型,表示地址为0,不指向有效数据。指向不同的数据类型(不包括void类型)的指针之间也存在明晰转化。void类型转化为其他托管类型的值针类型时需明晰转化,反之有其他指针类型转化为void类型时为隐含转化,可以自动进行。C#不保证指针类型之间的转化是安全的,可以通过C#的异常捕捉机制来处理这种可能的情况。
和C/C++类似,指针类型可以参与相当多的表达式运算。除了前面已经接触到的如指针赋值运算(*p=value),取地址运算(p=&value),还有结构成员获取运算(p->value),指针元素获取运算(p[0]),指针增量与减量运算(p++,p--,++p,--p),sizeof运算(sizeof(unmanaged-type)),指针之间的比较运算,以及指针和整数(int,uint,long,ulong)之间、指针之间的加减运算,这些表达式运算都需放置在unsafe标识的代码中,否则会在编译时出错。
{……}
public static void Main()
{
unsafe//unsafe语句块
{
Point pt=new Point(&a,&b);
Console.WriteLine(“/n--Swap Value--/n”);
……
}
}
}
在上面的程序中,出现了三种典型的unsafe上下文,我们用unsafe 来修饰Point结构类型,从而可以在该结构内任意地方操作指针。注意其中的成员声明语句“public int* x,y;”声明x和y都为指向整数的指针,这在传统C/C++中需要用语句“public int *x,*y;”。在Test类中的Swap方法声明中加上unsafe修饰后,便可以传入指针类型的参数,并在方法体内进行指针操作。在Main函数中,我们则采用了unsafe语句块的处理方式,这使得语句块内可以进行指针存取操作。
需要指出的是,“非安全代码并非不安全”!它仅仅是指示其中的内存不受自动垃圾收集器管理,而需要我们像以前在C/C++中那样自己负责分配和释放。
指针类型
指针类型和托管环境下的引用类型有点相似,数据包含在它们指向的内存区块。但指针类型和托管环境的引用类型有着本质的区别——自动垃圾收集器不追踪指针指向的数据区块,实际上自动垃圾收集器根本不知道指针及其数据的存在!
指针类型包含其指向的内存块的数据类型,这个数据类型在C#中被限制为“非托管类型”和“void类型”,“非托管类型”不能是引用类型。实际上,指针类型指向的内存区块根本不能是引用类型,也不能是包含引用类型的自定义结构。但引用类型可以包含指针类型,引用类型会被自动垃圾收集器管理,而声明为指针指向的“非托管类型”不被自动垃圾收集器管理!
指针在传递参数的时候,也可以用out和ref来表达传址(传引用),从而可以在函数内改变指针变量本身(也就是变量的地址)。和C/C++中的传址方式一样,如果在这样的函数中将指针的值改变为函数内局部变量的地址,在退出函数时,由于系统往往会回收这些地址空间,程序便会发生异常。看下面的例子:
using System;
class Test{
unsafe static void F(out int* pi1, ref int* pi2) {
int i=100;
int j=200;
pi1 = &i;
pi2 = &j;
Console.WriteLine((int)pi1+“,”+(int)pi2);
}
unsafe static void Main() {
int i = 10;
int* px1 = &i;
int* px2 = &i;
Console.WriteLine((int)px1+“,”+(int)px2);
F(out px1, ref px2);
Console.WriteLine((int)px1+“,”+(int)px2);
Console.WriteLine(“*px1 = {0}, *px2 = {1}”,*px1, *px2);
}
}
程序输出:
1243332,1243332
1243304,1243308
1243304,1243308
*px1 = 13249636, *px2 = 13253284
注意前三行的结果依赖于特定的执行环境,最后一行结果则属于未定义的程序行为。可以看到,函数F内的变量i、 j的存储空间在退出函数栈后被回收,指针变量px1、px2指向的数值将不确定。鉴于此,我们一般不要用out修饰指针类型参数,只有在确定改变的地址空间在退出函数后不会被回收,才可以使用ref来修饰指针类型参数。
指针类型可以和8种C#的整数类型之间进行转换,这种转换必须用括号“()”的形式进行明晰转化。空类型“null”也可以作为指针类型,表示地址为0,不指向有效数据。指向不同的数据类型(不包括void类型)的指针之间也存在明晰转化。void类型转化为其他托管类型的值针类型时需明晰转化,反之有其他指针类型转化为void类型时为隐含转化,可以自动进行。C#不保证指针类型之间的转化是安全的,可以通过C#的异常捕捉机制来处理这种可能的情况。
和C/C++类似,指针类型可以参与相当多的表达式运算。除了前面已经接触到的如指针赋值运算(*p=value),取地址运算(p=&value),还有结构成员获取运算(p->value),指针元素获取运算(p[0]),指针增量与减量运算(p++,p--,++p,--p),sizeof运算(sizeof(unmanaged-type)),指针之间的比较运算,以及指针和整数(int,uint,long,ulong)之间、指针之间的加减运算,这些表达式运算都需放置在unsafe标识的代码中,否则会在编译时出错。
非安全代码
C#没有像C/C++那样的内存动态分配语法(分配于堆上),只提供了应用于局部非托管类型变量的栈分配语句:stackalloc unmanaged-type [expression],其中expression为整数的表达式或者常量。栈分配空间不需要我们清除,函数退出后自动回收。看下面的例子:
using System;
class Test {
unsafe static string IntToString(int value) { char* buffer = stackalloc char[16];
……
}
static void Main() {
Console.WriteLine(IntToString(12345));
Console.WriteLine(IntToString(-999));
}
}
函数IntToString实现整数到字符串的转换,其中分配了buffer缓冲字符数组,用来暂时存放转换的字符变量。如果系统内存不够,栈分配语句会抛出System.StackOverflowException异常,这可以用try语句来捕捉。值得注意的是C#规定栈分配语句不可以放在catch或finally语句块内。
C#没有提供动态分配语法,但如果需要该怎么办?答案是通过互操作功能来调用特定平台的动态分配服务。看下面的例子:
using System.Runtime.InteropServices;
using System;
public unsafe class Memory{
static int ph = GetProcessHeap();
//获得进程堆的句柄
private Memory() {}
public static void* Alloc(int size)
//内存分配
{ void* result = HeapAlloc(ph, HEAP_ZERO_MEMORY, size);
if (result == null) throw new OutOfMemoryException();
return result;
}
public static void Free(void* block)
//内存释放
{if (!HeapFree(ph, 0, block)) throw new InvalidOperationException();
}
const int HEAP_ZERO_MEMORY = 0x00000008;//内存起始地址
[DllImport(“kernel32”)]
static extern int GetProcessHeap();
[DllImport(“kernel32”)]
static extern void* HeapAlloc(int hHeap, int flags, int size);
[DllImport(“kernel32”)]
static extern bool HeapFree(int hHeap, int flags, void* block);
}
我们通过调用Win32平台上的kernel32.dll库内的GetProcessHeap、HeapAlloc和HeapFree实现了Memory类的动态内存分配和释放功能。注意这里是在堆上进行内存分配的,我们必须自己负责释放!