非安全代码

非安全代码

我们知道,.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类的动态内存分配和释放功能。注意这里是在堆上进行内存分配的,我们必须自己负责释放!  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值