一、概念
- 指定对齐值:编译器或者程序员指定的对齐值,packLen,.Net中默认为4。
- 默认对齐值:结构体中每一个数据成员及结构体本身都有默认对齐值,记为defaultLen
- 成员偏移量:即相对结构体起始位置的长度,记为offset
- 成员长度:结构体中每个数据成员的长度(注意结构体成员为补齐之后的长度),记为memberLen
- 有效对齐值:validLen=min(packLen,defaultLen)。
两个规则:
- 对齐规则:offset%validLen=0,其中validLen=min(packLen,defaultLen)。
- 填充规则:如成员变量不遵守对齐规则,则不需要对齐补齐;再其前面填充一些字节保证该成员对齐。需填充的字节数记为pad。
- 结构体的总大小,也就是sizeof的结果,必须是其内部成员最大有效对齐值的整数倍,不足的要补齐
二、示例
1、成员之前需要补齐
public struct Person
{
public byte A ;
public UInt16 B;
}
static void Main(string[] args)
{
Person p = new Person();
unsafe
{
Console.WriteLine(((int)&p.A).ToString("X"));//D777EAF0
Console.WriteLine(((int)&p.B).ToString("X"));//D777EAF2
}
Console.WriteLine(Marshal.SizeOf(p));//4
}
指定对齐值packLen为4(默认的没有写出来)。有效对齐值=min(packLen,defaultLen)。
- A的偏移量为0,该字段有效对齐值为1,满足对齐方式,占用1byte。
- 下一个可用地址的偏移量为1,不是字段B的有效对齐值的倍数,所以需要补齐一个字节使偏移量变为2。所以需要字段A后面补足一个字节,字段A占两个字节。保证B的偏移量为2,满足对齐方式,B占2字节。
- 所有成员都分配了空间,空间总大小为2+2=4byte,是最大有效对齐值2的倍数,所以最终大小为4byte。
1、结构体最后需要补齐
public struct Person
{
public byte A;
public UInt16 B;
public byte C;
}
static void Main(string[] args)
{
Person p = new Person();
unsafe
{
Console.WriteLine(((int)&p.A).ToString("X"));//47BAE920
Console.WriteLine(((int)&p.B).ToString("X"));//47BAE922
Console.WriteLine(((int)&p.C).ToString("X"));//47BAE924
}
Console.WriteLine(Marshal.SizeOf(p));//6
}
- 字段A偏移量为0,有效对齐值为1,满足对齐方式,占1一个字节。
- 下一个字段的偏移量为1,有效对齐值为2,不满足对齐方式。所以需要在A后面补齐一个字节,使下一个字段的偏移量为2。
- 所以字段A需要占两个字节。B的偏移量为2,B占两个字节。
- C字段的偏移量为4,有效对齐值为1,满足对齐方式,占用一个字节。
- 总成员占2+2+1=5,不是最大有效对齐值2的倍数,所以最后一个需要补齐1个字节。总字节为2+2+1+1=6。
3、结构体中成员顺序
public struct Person
{
public byte A;
public byte C;
public UInt16 B;
}
static void Main(string[] args)
{
Person p = new Person();
unsafe
{
Console.WriteLine(((int)&p.A).ToString("X"));//FE57EAC0
Console.WriteLine(((int)&p.B).ToString("X"));//FE57EAC2
Console.WriteLine(((int)&p.C).ToString("X"));//FE57EAC1
}
Console.WriteLine(Marshal.SizeOf(p));//4
}
还是上面的一个例子,我们调整了一下顺序,正好把成员C放在了A后面,这样A后面就不需要补齐一个字节了,结构体总共占4字节。所以在结构体成员中尽量按所占字节从小到大排列,这样可能会使整个结构体占用空间变小。
三、StructLayout特性
1、LayoutKind枚举类型。
允许的值有StructLayout.Auto 、StructLayout.Sequential 和StructLayout.Explicit。默认的内存排列就是Sequential,也就是按成员的先后顺序排列;Explicit可以精确布局内存,需要用FieldOffset()设置每个成员的位置,这样就可以实现类似c的公用体的功能。
2、CharSet
CharSet定义在结构中的字符串成员在结构被传给DLL时的排列方式。可以是Unicode、Ansi或Auto,默认为Auto。指示在默认情况下是否应将类中的字符串数据字段作为 LPWSTR
或 LPSTR
进行封送处理。
3、Pack
Pack定义了结构的封装大小。可以是1、2、4、8、16、32、64、128或特殊值0。特殊值0表示当前操作平台默认的压缩大小。
4、Size
指示类或结构的绝对大小
5、示例
修改指定对齐值
[StructLayout(LayoutKind.Sequential,Pack = 1)]
public struct Person
{
public double A;
public byte C;
public byte B;
}
static void Main(string[] args)
{
Person p = new Person();
unsafe
{
Console.WriteLine(((int)&p.A));//-380116104
Console.WriteLine(((int)&p.B));//-380116095
Console.WriteLine(((int)&p.C));//-380116096
}
Console.WriteLine(Marshal.SizeOf(p))//10
}
这里就不做解释了,只是修改了指定对齐值为1。
精确布局内存
[StructLayout(LayoutKind.Explicit,Size =12)]
public struct Person
{
[FieldOffset(0)]
public double A;
[FieldOffset(8)]
public byte B;
[FieldOffset(9)]
public byte C;
[FieldOffset(10)]
public UInt16 D;
}
static void Main(string[] args)
{
Person p = new Person();
unsafe
{
Console.WriteLine(((int)&p.A));//1402463224
Console.WriteLine(((int)&p.B));//1402463232
Console.WriteLine(((int)&p.C));//1402463233
Console.WriteLine(((int)&p.D));//1402463234
}
Console.WriteLine(Marshal.SizeOf(p));//12
}
Explicit一般与Size一起使用,不然最终的大小不是期待的。
联合体
[StructLayout(LayoutKind.Explicit,Size =1)]
public struct Person
{
[FieldOffset(0)]
public byte A;
[FieldOffset(0)]
public byte B;
}
static void Main(string[] args)
{
Person p = new Person();
unsafe
{
Console.WriteLine(((int)&p.A));//1689773680
Console.WriteLine(((int)&p.B));//1689773680
}
Console.WriteLine(Marshal.SizeOf(p));//1
p.A = 100;
Console.WriteLine(p.B);//100
}