C#锐利体验
南京邮电学院 李建忠( lijianzhong@263.net.cn )
第十八讲 非安全代码
.NET通用语言运行时为C#引入了一种托管的安全编程方式。指针存取,变量地址计算,对象销毁等等操作在托管编程环境下都是C#所不允许的,这大大改进了传统C/C++的安全问题。但事物往往是多面性的,在我们摈除指针等内存的直接存取方式的同时,我们也丧失了它在某些问题上的便利性,比如某些和操作系统底层的交互,内存映射设备的存取等等。在某些特殊的任务上,我们甚至不希望引入自动垃圾收集这种“不确定的系统消耗”。C#通过引入非安全(unsafe)代码来迎合这些特殊需要。
非安全代码
???????在非安全代码中,C#允许我们直接操作指针,获取变量地址,进行指针类型与整数类型之间的转换等等在C/C++中经常出现的操作。非安全代码通过关键字“unsafe”来标志。编译非安全代码时必须加编译选项“/unsafe”,否则编译器报错。“unsafe”关键字可以加之于类型(类,结构,接口,委派)声明,成员(构造器,析构器,域,方法,属性,事件,索引器,操作符)声明,以及用大括号“{}”括起来的语句块。标志了unsafe的代码块又称为unsafe上下文。看下面的例程:
程序输出:
初始值 :
Values:??*pt.x= 1, *pt.y= 2
Address: pt.x= 1243332, pt.y= 1243328
换值之后 :
Values:??*pt.x= 2, *pt.y= 1
Address: pt.x= 1243332, pt.y= 1243328
换址之后 :
Values:??*pt.x= 1, *pt.y= 2
Address: pt.x= 1243328, pt.y= 1243332
在上面的程序中,我们演示了典型的三种unsafe上下文。我们用unsafe 来修饰Point结构类型,从而可以在该结构内任意地方操作指针。
注意其中的成员声明语句“public int* x,y;”声明x和y都为指向整数的指针,这在传统C/C++中需要同时在x,y前面加星号(*),如“public int* x,*y;”。
在Test类中的Swap方法声明中加上unsafe修饰后,便可以传入指针类型的参数,并在方法体内进行指针操作。
在Main函数中,我们则采用了unsafe语句块的处理方式,这使得语句块内可以进行指针存取操作。两个Swap函数,一个交换指针指向的数据,一个交换指针地址,我们从程序的输出也可以看到这一点。
需要指出的是,“非安全代码并非不安全”!它仅仅是指示其中的内存不受自动垃圾收集器管理,而需要我们象以前在C/C++中那样自己负责分配和释放。
指针类型
???????指针类型和我们前面的托管环境下的引用类型有点相似,类型本身都不包含数据,数据包含在他们指向的内存区块。但指针类型和托管环境的引用类型有着本质的区别——指针指向的数据区块不受自动垃圾收集器追踪,而引用句柄指向的数据对象却受自动垃圾收集器追踪、管理。实际上自动垃圾收集器根本不知道指针及其数据的存在!
???????指针类型包含其指向的内存块的数据类型,这个数据类型在C#中被限制为“非托管类型”和“void类型”。其中“非托管类型”定义为下列类型之一:
1.??sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal和bool;
2.??枚举类型;
3.??自定义结构类型,其成员变量只能为“非托管类型”;
4.??指针类型。
可以看到,“非托管类型”不能是引用类型。实际上,指针类型指向的内存区块根本不能是引用类型,也不能是包含引用类型的自定义结构。但引用类型可以包含指针类型,这是为什么呢?理解这一点并不困难,因为引用类型会被自动垃圾收集器管理,而声明为指针指向的“非托管类型”不被自动垃圾收集器管理!
???????虽然指针在传递参数的时候,也可以用out和ref来表达传址(传引用),从而使得我们可以在函数内改变指针变量本身(也就是变量的地址),本文第一个例子中的Swap函数就这样做的。但和C/C++中的传递指针参数一样,如果我们在这样的函数中将指针的值改变为函数内局部变量的地址,在我们退出函数时,由于系统往往会回收这些地址空间,程序便会发生未定义的行为。看下面的例子:
程序输出:
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标志的代码中,否则引起编译时错误。
fixed 语句
???????从内存管理的角度可以把C#中的变量分为两类,一类是固定变量,另一类是可动变量。固定变量一般寄宿于方法调用的栈中,它们的内存不受自动垃圾收集器管理。
固定变量的例子有非托管类型的局部变量及其实例域成员,非托管类型的传值参数,以及提领指针所创建的变量等。一个判别固定变量的方法是看它是否可以无任何限制地用符号&来获取它的地址。看下面的例程:
???????可动变量寄宿于托管堆中,它们的内存全权交由自动垃圾收集器管理。可动变量的例子有引用类型对象的实例域成员,任何类型的静态域成员,引用或者输出参数,数组元素等。不可以直接用符号&获取可动变量的地址,C#支持用fixed语句来暂时性地获取可动变量的地址。该地址只有在紧跟在fixed后面的语句块内才有效。看下面的例程:
sizeof 操作符
???????sizeof操作符可以用来计算非托管类型的在栈分配时的存储尺寸,它的结果是以字节为单位的整数。sizeof操作符的操作对象是托管类型,变量不可以做sizeof操作符的参数。
???????对于C#中的简单类型,由于他们的存储空间已被系统确定,sizeof返回他们的所占字节数为常数。对于自定义枚举类型,sizeof返回该枚举类型指定的基类型的存储尺寸,如果没有指定枚举的基类型,C#默认为32位的整数类型。对于指针类型,它的存储尺寸和32位的整数类型相同,自然sizeof会返回4个字节。看下面的例程:
程序输出:
sizeof(sbyte) :1?????????sizeof(byte) 1
sizeof(short) :2?????????sizeof(ushort) :2
sizeof(int) :4??????????sizeof(uint) :4
sizeof(long) :8??????????sizeof(ulong) :8
sizeof(char) :2??????????sizeof(bool) :1
sizeof(float) :4??????????sizeof(double) :8
sizeof(MyEnum) :1??????????sizeof(int*) :4?????????
对于托管的自定义结构类型,sizeof在计算它的存储尺寸时,要考虑各域成员变量的存储空间在排列时的情况。如果没有用StructLayout特征来明确指定,托管自定义结构类型的域成员变量的存储排列方式将由CLR运行时负责,这时往往会为了排列对齐而对结构的存储空间做一定的填充,也就是说。值得指出的是,成员变量声明的顺序不同,也会导致CLR运行时对它们作不同的排列,进而会有不同的填充行为,也可能会导致sizeof返回值的不同。对于用StructLayout特征明确指定的托管自定义结构,sizeof在要考虑按指定的要求排列的同时,也要考虑对齐填充的问题。看下面的例程:
程序输出:
sizeof(MyStruct1) :16
sizeof(MyStruct2) :24
sizeof(MyUnion) :8
???????我们看到对于所有的域变量都相同的非托管自定义结构MyStruct1和MyStruct2,仅仅由于我们将MyByte域的声明位置放在不同的地方,它们的sizeof的返回值便不同。MyUnion结构由于我们用StructLayout特征指定了它的域成员的存储布局,它的sizeof返回值和其中占位最长的域变量MyLong的返回值相同,这和它作为“联合”的行为是一致的。
内存分配
???????C#没有象C/C++那样的内存动态分配语法(分配于堆上),只提供了应用于局部非托管类型变量的栈分配语句:stackalloc unmanaged-type [ expression ] ,其中expression为整数的表达式或者常量。栈分配空间不需要我们自己清除,函数退出后自动回收。看下面的例子:
函数IntToString实现整数到字符串的转换,其中我们分配了buffer缓冲字符数组,用来暂时存放转换的字符变量。如果系统内存不够,栈分配语句会抛出System.StackOverflowException异常。这可以用try语句来捕捉。值得指出的是C#规定栈分配语句不可以放在catch或finally语句块内。
???????C#没有提供动态分配语法,但我们如果需要该怎么办?答案是通过引入外部方法来调用特定平台的动态分配服务.看下面的例子:
我们通过调用Win32平台上的kernel32.dll库内的GetProcessHeap, HeapAlloc和HeapFree实现了Memory类的动态内存分配和释放功能.注意这里是在堆上进行内存动态分配,我们必须自己负责释放!
南京邮电学院 李建忠( lijianzhong@263.net.cn )
第十八讲 非安全代码
.NET通用语言运行时为C#引入了一种托管的安全编程方式。指针存取,变量地址计算,对象销毁等等操作在托管编程环境下都是C#所不允许的,这大大改进了传统C/C++的安全问题。但事物往往是多面性的,在我们摈除指针等内存的直接存取方式的同时,我们也丧失了它在某些问题上的便利性,比如某些和操作系统底层的交互,内存映射设备的存取等等。在某些特殊的任务上,我们甚至不希望引入自动垃圾收集这种“不确定的系统消耗”。C#通过引入非安全(unsafe)代码来迎合这些特殊需要。
非安全代码
???????在非安全代码中,C#允许我们直接操作指针,获取变量地址,进行指针类型与整数类型之间的转换等等在C/C++中经常出现的操作。非安全代码通过关键字“unsafe”来标志。编译非安全代码时必须加编译选项“/unsafe”,否则编译器报错。“unsafe”关键字可以加之于类型(类,结构,接口,委派)声明,成员(构造器,析构器,域,方法,属性,事件,索引器,操作符)声明,以及用大括号“{}”括起来的语句块。标志了unsafe的代码块又称为unsafe上下文。看下面的例程:
using System; unsafe struct Point//unsafe类型 { ?????public int* x,y; ?????public Point(int* a,int* b) ?????{ ?????????x=a; ?????????y=b; ?????} } class Test { ?????public static void Swap(ref int x, ref int y) ?????{ ?????????int a; ?????????a=x; x=y; y=a; ?????} ?????public static unsafe void Swap(ref int *x,ref int *y)//unsafe成员 ?????{ ?????????int* a; ?????????a=x; x=y; y=a; ?????} ?????public static void Main() ?????{ ?????????int a=1,b=2; ?????????unsafe//unsafe语句块 ?????????{ ??????????????Point pt=new Point(&a,&b);//初始化 ??????????????Console.WriteLine("初始值 :"); ??????????????Console.WriteLine("Values:??*pt.x= {0}, *pt.y= {1}", *pt.x, *pt.y); ??????????????Console.WriteLine("Address: pt.x= {0}, pt.y= {1}",(int)pt.x, (int)pt.y); ?????????????? ??????????????Swap(ref *pt.x,ref *pt.y);//交换值 ??????????????Console.WriteLine("换值之后 :"); ??????????????Console.WriteLine("Values:??*pt.x= {0}, *pt.y= {1}", *pt.x, *pt.y); ??????????????Console.WriteLine("Address: pt.x= {0}, pt.y= {1}",(int)pt.x, (int)pt.y); ???????????????????????????? ??????????????Swap(ref pt.x, ref pt.y);//交换地址 ??????????????Console.WriteLine("换址之后 :"); ??????????????Console.WriteLine("Values:??*pt.x= {0}, *pt.y= {1}", *pt.x, *pt.y); ??????????????Console.WriteLine("Address: pt.x= {0}, pt.y= {1}",(int)pt.x, (int)pt.y); ?????????} ?????} } |
程序输出:
初始值 :
Values:??*pt.x= 1, *pt.y= 2
Address: pt.x= 1243332, pt.y= 1243328
换值之后 :
Values:??*pt.x= 2, *pt.y= 1
Address: pt.x= 1243332, pt.y= 1243328
换址之后 :
Values:??*pt.x= 1, *pt.y= 2
Address: pt.x= 1243328, pt.y= 1243332
在上面的程序中,我们演示了典型的三种unsafe上下文。我们用unsafe 来修饰Point结构类型,从而可以在该结构内任意地方操作指针。
注意其中的成员声明语句“public int* x,y;”声明x和y都为指向整数的指针,这在传统C/C++中需要同时在x,y前面加星号(*),如“public int* x,*y;”。
在Test类中的Swap方法声明中加上unsafe修饰后,便可以传入指针类型的参数,并在方法体内进行指针操作。
在Main函数中,我们则采用了unsafe语句块的处理方式,这使得语句块内可以进行指针存取操作。两个Swap函数,一个交换指针指向的数据,一个交换指针地址,我们从程序的输出也可以看到这一点。
需要指出的是,“非安全代码并非不安全”!它仅仅是指示其中的内存不受自动垃圾收集器管理,而需要我们象以前在C/C++中那样自己负责分配和释放。
指针类型
???????指针类型和我们前面的托管环境下的引用类型有点相似,类型本身都不包含数据,数据包含在他们指向的内存区块。但指针类型和托管环境的引用类型有着本质的区别——指针指向的数据区块不受自动垃圾收集器追踪,而引用句柄指向的数据对象却受自动垃圾收集器追踪、管理。实际上自动垃圾收集器根本不知道指针及其数据的存在!
???????指针类型包含其指向的内存块的数据类型,这个数据类型在C#中被限制为“非托管类型”和“void类型”。其中“非托管类型”定义为下列类型之一:
1.??sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal和bool;
2.??枚举类型;
3.??自定义结构类型,其成员变量只能为“非托管类型”;
4.??指针类型。
可以看到,“非托管类型”不能是引用类型。实际上,指针类型指向的内存区块根本不能是引用类型,也不能是包含引用类型的自定义结构。但引用类型可以包含指针类型,这是为什么呢?理解这一点并不困难,因为引用类型会被自动垃圾收集器管理,而声明为指针指向的“非托管类型”不被自动垃圾收集器管理!
???????虽然指针在传递参数的时候,也可以用out和ref来表达传址(传引用),从而使得我们可以在函数内改变指针变量本身(也就是变量的地址),本文第一个例子中的Swap函数就这样做的。但和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标志的代码中,否则引起编译时错误。
fixed 语句
???????从内存管理的角度可以把C#中的变量分为两类,一类是固定变量,另一类是可动变量。固定变量一般寄宿于方法调用的栈中,它们的内存不受自动垃圾收集器管理。
固定变量的例子有非托管类型的局部变量及其实例域成员,非托管类型的传值参数,以及提领指针所创建的变量等。一个判别固定变量的方法是看它是否可以无任何限制地用符号&来获取它的地址。看下面的例程:
using System; struct Point { ?????public int x; ?????public int y; ?????public int z; } unsafe class Test { ?????static void Main() ?????{ ?????????int integer=100;??? ?????????PrintAddress(integer); ?????????int* ptInteger=&integer; ?????????Console.WriteLine((int)&(*ptInteger));//提领指针所创建的变量 ?????????Point point=new Point(); ?????????Console.WriteLine((int)&point);//非托管类型局部变量 ?????????Console.WriteLine((int)&point.x);//非托管类型局部变量的实例成员 ?????????Console.WriteLine((int)&point.y);//非托管类型局部变量的实例成员 ?????????Console.WriteLine((int)&point.z);//非托管类型局部变量的实例成员 ?????} ?????static void PrintAddress(int i) ?????{ ?????????Console.WriteLine((int)&i);//非托管类型的传值参数 ?????} } |
???????可动变量寄宿于托管堆中,它们的内存全权交由自动垃圾收集器管理。可动变量的例子有引用类型对象的实例域成员,任何类型的静态域成员,引用或者输出参数,数组元素等。不可以直接用符号&获取可动变量的地址,C#支持用fixed语句来暂时性地获取可动变量的地址。该地址只有在紧跟在fixed后面的语句块内才有效。看下面的例程:
using System; class MyClass {???? ?????public static int StaticField; ?????public int InstanceField; } unsafe class Test { ?????static void PrintAddress(int* point) ?????{ ?????????Console.WriteLine((int)point); ?????} ?????public static void Main() ?????{ ?????????MyClass myObject = new MyClass(); ?????????int[] arr = new int[10]; ?????????fixed (int* p = &MyClass.StaticField) PrintAddress(p); ?????????fixed (int* p = &myObject.InstanceField) PrintAddress(p); ?????????fixed (int* p = &arr[0]) PrintAddress(p); ?????????fixed (int* p = arr) PrintAddress(p); ?????} } |
sizeof 操作符
???????sizeof操作符可以用来计算非托管类型的在栈分配时的存储尺寸,它的结果是以字节为单位的整数。sizeof操作符的操作对象是托管类型,变量不可以做sizeof操作符的参数。
???????对于C#中的简单类型,由于他们的存储空间已被系统确定,sizeof返回他们的所占字节数为常数。对于自定义枚举类型,sizeof返回该枚举类型指定的基类型的存储尺寸,如果没有指定枚举的基类型,C#默认为32位的整数类型。对于指针类型,它的存储尺寸和32位的整数类型相同,自然sizeof会返回4个字节。看下面的例程:
using System; enum MyEnum:byte { ?????北京, ?????上海, ?????南京 } class Test { ?????public unsafe static void Main() ?????{ ?????????Console.Write("sizeof(sbyte) :{0}??/t",sizeof(sbyte)); ?????????Console.WriteLine("sizeof(byte) "+sizeof(byte)); ?????????Console.Write("sizeof(short) :{0}??/t",sizeof(short)); ?????????Console.WriteLine("sizeof(ushort) :"+sizeof(ushort)); ?????????Console.Write("sizeof(int) :{0}???/t",sizeof(int)); ?????????Console.WriteLine("sizeof(uint) :"+sizeof(uint)); ?????????Console.Write("sizeof(long) :{0}???/t",sizeof(long)); ?????????Console.WriteLine("sizeof(ulong) :"+sizeof(ulong)); ?????????Console.Write("sizeof(char) :{0}???/t",sizeof(char)); ?????????Console.WriteLine("sizeof(bool) :"+sizeof(bool)); ?????????Console.Write("sizeof(float) :{0}???/t",sizeof(float)); ?????????Console.WriteLine("sizeof(double) :"+sizeof(double)); ?????????Console.Write("sizeof(MyEnum) :{0}???/t",sizeof(MyEnum)); ?????????Console.WriteLine("sizeof(int*) :{0}???/t",sizeof(int*)); ?????} } |
程序输出:
sizeof(sbyte) :1?????????sizeof(byte) 1
sizeof(short) :2?????????sizeof(ushort) :2
sizeof(int) :4??????????sizeof(uint) :4
sizeof(long) :8??????????sizeof(ulong) :8
sizeof(char) :2??????????sizeof(bool) :1
sizeof(float) :4??????????sizeof(double) :8
sizeof(MyEnum) :1??????????sizeof(int*) :4?????????
对于托管的自定义结构类型,sizeof在计算它的存储尺寸时,要考虑各域成员变量的存储空间在排列时的情况。如果没有用StructLayout特征来明确指定,托管自定义结构类型的域成员变量的存储排列方式将由CLR运行时负责,这时往往会为了排列对齐而对结构的存储空间做一定的填充,也就是说。值得指出的是,成员变量声明的顺序不同,也会导致CLR运行时对它们作不同的排列,进而会有不同的填充行为,也可能会导致sizeof返回值的不同。对于用StructLayout特征明确指定的托管自定义结构,sizeof在要考虑按指定的要求排列的同时,也要考虑对齐填充的问题。看下面的例程:
using System; using System.Runtime.InteropServices; struct MyStruct1 { ?????public byte MyByte;// 1 byte ?????public short MyShort;// 2 bytes ?????public int MyInt;// 4 bytes ?????public long MyLong;// 8 bytes } struct MyStruct2 { ?????public short MyShort;// 2 bytes ?????public int MyInt;// 4 bytes ?????public long MyLong;// 8 bytes???? ?????public byte MyByte;// 1 byte } [StructLayout(LayoutKind.Explicit)] struct MyUnion { ?????[FieldOffset(0)] ?????public byte MyByte;//从零位开始的byte ?????[FieldOffset(0)] ?????public short MyShort;//从零位开始的short ?????[FieldOffset(0)] ?????public int MyInt;//从零位开始的int ?????[FieldOffset(0)] ?????public long MyLong;//从零位开始的long } class Test { ?????public unsafe static void Main() ?????{ ?????????Console.WriteLine("sizeof(MyStruct1) :{0}",sizeof(MyStruct1)); ?????????Console.WriteLine("sizeof(MyStruct2) :{0}",sizeof(MyStruct2)); ?????????Console.WriteLine("sizeof(MyUnion) :{0}",sizeof(MyUnion)); ?????} } |
程序输出:
sizeof(MyStruct1) :16
sizeof(MyStruct2) :24
sizeof(MyUnion) :8
???????我们看到对于所有的域变量都相同的非托管自定义结构MyStruct1和MyStruct2,仅仅由于我们将MyByte域的声明位置放在不同的地方,它们的sizeof的返回值便不同。MyUnion结构由于我们用StructLayout特征指定了它的域成员的存储布局,它的sizeof返回值和其中占位最长的域变量MyLong的返回值相同,这和它作为“联合”的行为是一致的。
内存分配
???????C#没有象C/C++那样的内存动态分配语法(分配于堆上),只提供了应用于局部非托管类型变量的栈分配语句:stackalloc unmanaged-type [ expression ] ,其中expression为整数的表达式或者常量。栈分配空间不需要我们自己清除,函数退出后自动回收。看下面的例子:
using System; class Test { ?????unsafe static string IntToString(int value) ?????{ ?????????char* buffer = stackalloc char[16]; ?????????char* p = buffer + 16; ?????????int n = value >= 0? value: -value; ?????????do ?????????{ ??????????????*(--p) = (char)(n % 10 + '0'); ??????????????n /= 10; ?????????} while (n != 0); ?????????if (value < 0) *--p = '-'; ?????????return new string(p, 0, (int)(buffer + 16 - p)); ?????} ?????public 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); } class Test { ?????unsafe static void Main() ?????{ ?????????byte* buffer = (byte*)Memory.Alloc(256); ?????????for (int i = 0; i < 256; i++) ??????????????buffer[i] = (byte)i; ?????????for (int i = 0; i < 256; i++) ??????????????Console.WriteLine(buffer[i]); ?????????Memory.Free(buffer); ?????} } |
我们通过调用Win32平台上的kernel32.dll库内的GetProcessHeap, HeapAlloc和HeapFree实现了Memory类的动态内存分配和释放功能.注意这里是在堆上进行内存动态分配,我们必须自己负责释放!