欢迎来到.net学习网

欢迎联系站长一起更新本网站!QQ:879621940

您当前所在位置:首页 »  .NET本质论第一卷:公共语言运行库教程 » 正文

本教程章节列表

类型的布局:auto、sequential和explicit

创建时间:2013年05月20日 19:09  阅读次数:(2823)
分享到:
与使用传统C的指针相比,使用非托管指针需要注意更多的细节。这是因为非托管指针需要“尊重”由CLR所“拥有”的内存。当涉及到垃圾回收的堆上的内存时,这个问题尤其棘手。

CLR能够(并且将要)在垃圾回收堆中重新定位对象。这意味着获取对象中字段的地址时要格外谨慎,因为垃圾回收器在压缩堆时不会调整非托管指针。为了使非托管指针能够引用对象中的字段,CLR允许非托管指针被声明为pinned(钉住)。一个pinned指针在其作用域中会防止周围对象被重新定位。每种编程语言公开的pinned指针都不相同。在c++中,你可以使用_pin指针修饰符。在C#中,你可以使用fixed语句,如下所示:
public unsafe class Bob{
double y; //对象中的实例对象
static public void Hello(Bob b){
fixed(double *py=&b.y){
System.Console.WriteLine((int)py);
*py-300.00;
}
Debug.Assert(b.y==300.00)
}
}

在fixed语句的作用域内,b所引用的对象被保证不能移动,即使Console.WriteLine阻塞相当长的时间。然而,对象可能在执行Hello方法的最后一条语句之前移动。如果这种情形发牛,垃圾回收器将相应地调整b引用。然而请注意,如果CLR在pinned指针还没有超出作用域时进行一次垃圾回收,那么,它将不能重新定位底层对象。基于这个原因,程序员应当尽量短时间地占用指针,以避免堆碎片。

程序员不能缓存非托管指针到垃圾回收的堆内存中,这一点很重要。例如,下面的程序将导致崩溃:
unsafe class Bob{
static int *pi;
int x;
static void Main(){
Bob o=new Bob();
fixed(int *p=&o.x){
pi=p //当心!!
}
System.GC.Collect(2);//强制的内存活动(垃圾回收)
*pi=100; //使用旧的prt
System.Console.WriteLine(o.x);
}
}

这段程序能够被顺利地编译,而没有警告和错误。然而,将pinned指针(p)赋值给非pinned指针(pi),应该引起有经验的c#程序员的关注。

在这个示例中,在fixed语句之外使用pi将造成随机行为,接着,对System.GC.Collect方法的调用将加剧这种随机行为,然而,即使没有进行这样的调用,程序也是不合法的。因为垃圾回收可能在任何时间发生[由于库调用、其他线程的活动或者是使用同步垃圾回收器(concurrent garbage collcctor)]。

在访问堆栈的内存时,你不必使用pinned指针,这是很重要的。这意味着局部变量(或者值类型的局部变量的字段)的地址不需要任何特殊的处理。事实上,下面的代码将导致编译错误,因为CLR不会重新定位声明为局部变量的值:
public class Target{
public stativ void f(){
int x=0;//X在堆栈上不会移动
fixed(int *px=&x){ //错误:X已经固定!}
}
}

C++和C#只支持非托管指针的标量(scaled)加法和减法运算。为了执行任意数值的操作,你必须首先将其转换为数值类型。为此,CLR支持两个通用数值类型,以便保证足够大来持有指引。CLR总是通过System.Intptr和System.UIntPtr类型向程序员依次公开这些类型(native int 和native uint)。因为不同的架构使用不同的指针大小,所以,这两种类型的大小是直到运行时才能确定。下面的代码说明了native int的使用,它将一个指针的大小提升为8个字节:
static IntPtr RoundPtr(IntPtr ptr){
if(IntPtr.Size==4){ //专用于32位平台
int n=ptr.ToInt32();
n=(n+7) &-7;
return (IntPtr)n;
}
else if(IntPtr.Size==8){//专用于64位平台
long n=ptr.ToInt64();
n=(n+7)&-7;
return IntPtr|n;
}
else
 throw new exception(“Unknown pointer size”);
}

注意,上述代码使用System.IntPtr.Size属性选择适合的数值类型。并且利用了System.IntPtr在System.Int32、System.Int54或者void*之间的强制类型转换。这也使得System.IntPtr成为最适合表示win32句柄类型(例如,HANDLE或者HWND)的类型。

如果不处理类型的布局(layout),则很难运用指针。考察下面的C#程序:
using System;
unsafe class Bob{
short a;
double b;
short c;
static void Main(){
Bob bob=new Bob();
fixed(void *pa=&bob.a){
fixed(void *pb=&bob.b){
fixed(void *pc=&bob.c){
// 当心,这些断言能成功吗?
Debug.Assert(pa<pb);
Debug.Assert(pb<pc);
Debug.Asert(pa<pc);
}
}
}
}
}
 
用传统C或者c++编写等价程序,这三个断言都会成功。这是因为C和c++保证:类型的内存布局(in-memory layout)是基于声明的顺序。

如第3章所讨论的那样,CLR采用虚拟化的布局系统,并且基于性能特征进行布局。对于大多数程序而言,这是比较理想的。然而,对于那些基于类型内存格式的程序来说,它们需要显式地操作内存,因而要求某种机制覆盖自动布局的规则,并且能够显式地控制类型的布局。

有三种无数据特性用于控制类型的布局:auto、sequential和explicit。每种类型都明确地具有这些特性集之一,CLR在运行时计算标记为auto类型的布局,这些类型被称为“无布局’。CLR保证标记为sequential的类型将以声明顺序进行布局,它采用宿主平台:在win32上。这等价于Microsoft C++的#pragma pack 8选项)默认的组装规则。你可以通过其他的元数据项指定标记为explicit类型的精确格式,这些元数据项标明类型每一个单独字段的偏移量。标记为sequential或者explicit的类型有时也称为格式化的类型(formatted type)。因为它们的格式由程序员控制。

当发射用于类型的元数据时,编译器可以采用编程语言设计者推荐的任何特性。在C#和VB.NET中,类被默认标记为auto.struct则被默认标记为sequential。为了允许程序员以统一的方式表示布局特性。CLR定义了两个伪定制特性:System.Runtime.InteropoServices.StructLayout和System.Runtime.InteropoServices.FieldOffset。这些特性只是简单地通知编译器如何发射元数据,而不会在目标执行体中作为定制特性出现。为了便于理解这些特性的影响,考察下面的c#类型定义:
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Auto)]
public struct Jane{
public short a;
public double b;
public short c;
}

[StructLayout(LayoutKind.Sequential)]
public sruct Helen{
public short a;
public double b;
public short c;
}

[StructLayout(LayoutKind.Explicit)]
public sruct Betty{
[FieldOffset(0)] public short a;
[FieldOffset(8)] public double b;
[FieldOffset(2)] public short c;
}

这三个类型在逻辑上是等价的,但是每一个都有不同的内存表示。在笔者的机器上,Jane的字段顺利为{b,a,c},Helen的字段顺序为{a,b,c},而Betty的字段顺序为{a,c,b}。由于c#假定struct默认为sequential,所以Helen上的StructLayout特性是多余的。[祂 婳俌s
来源:.net学习网
说明:所有来源为 .net学习网的文章均为原创,如有转载,请在转载处标注本页地址,谢谢!
【编辑:Wyf】

打赏

取消

感谢您的支持,我会做的更好!

扫码支持
扫码打赏,您说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

最新评论

共有评论0条
  • 暂无任何评论,请留下您对本文章的看法,共同参入讨论!
发表评论:
留言人:
内  容:
请输入问题 0+50=? 的结果(结果是:50)
结  果: