C#结构体性质

在.Net程序开发中与外部系统(如嵌入式软件,C/C++)进行通信时,会调用一些非托管的库,DllImport方式调用这些库时需根据库的头文件写出对应的PInvoke方法,按照规约定义好方法的参数。这类库通常会自定义一些结构体类型做为参数或返回值,由于C/C++中的结构体与C#中的结构体struct并不完全相同,稍不注意就会数据错误,且这种错误不会抛出异常,所以极其隐蔽、很难发现。
现对C#中的strucr的特殊性质做些整理说明

  • 结构体的分配的内存大小与字段的排序有关
  • 结构体的分配的内存大小与各字段的类型字节相关

现举例说明,测试环境以Win10 *64 + .NetCore 2.1.403 + VS2017为测试环境,测量对象大小采用Marshal.SizeOf(var) ,测量类型大小采用sizeof(type)

基本类型分配内存大小

var a = sizeof(byte);//1
var b = sizeof(char);//2
var c = sizeof(int);//4
var d = sizeof(float);//4
var e = sizeof(long);//8
var f = sizeof(double);//8
var g = sizeof(decimal);//16

结构体大小

using System;
using System.Runtime.InteropServices;
public struct StructTest1
{
    public byte A;
    public int B;
    public double C;
}
public struct StructTest2
{
    public int A;
    public double B;
    public byte C;
}
public struct ReceiveStruct
{
    int X;
    char Y[8];
    double Z[3];
}
public struct StructTest3
{
    public int A;
    public long B;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3, ArraySubType = UnmanagedType.R8)]
    public double[] C;//长为3的double数组
}
public struct StructTest4
{
    public int A;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8, ArraySubType = UnmanagedType.I8)]
    public byte[] B;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3, ArraySubType = UnmanagedType.R8)]
    public double[] C;//长为3的double数组
}

StructTest1 st1 = new StructTest1();
var s1 = Marshal.SizeOf(st1);//16
StructTest2 st2 = new StructTest2();
var s2 = Marshal.SizeOf(st2);//24
StructTest3 st3 = new StructTest3();
StructTest4 st4 = new StructTest4();

由s1的大小可知结构体的大小并不是简单的将各个字段相加后的大小(1+4+8=13≠16)。这涉及到.Net在处理结构体时对其进行了特殊处理:结构体为值类型,存在栈上,为了避免多次访问栈,.Net对结构体进行了一个对齐操作——如果某个字段的长度大于或等于对齐长度,则对齐无用;如果小于对齐长度,则以对齐长度为单元,占用的空间是对齐长度的整数倍,同时保证不大于对齐长度的字段一定分配在同一单元,并且其首地址一定是字段长度的整数倍。若没有显式指定对齐长度,将以结构中占用空间最大成员的长度作为对齐长度。

根据对齐操作的说明,可知s1的内存分配情况为:第0个字节为字段A;第1-3个字节对齐操作自动补上的空字节,为无意义字节;第4-7个字节为字段B;第8-15个字节为字段C。

由s2与s1的对比可知结构体字段间的排序对整个结构体大小有影响。根据结构体分配在栈上的连续空间特点,并结合对齐操作,易知s2的内存分配情况为:第0-3字节为字段A;第4-7字节为补齐字节;第8-15为字段B;第16个字节为字段C;第18-23为补齐字节。

st3与st4的例子是在对接其他项目时遇到的一个问题示例。对方C++库的方法定义的结构体并没有遵循将大字节的字段放前面的原则,发过来的数据结构形如ReceiveStruct,在C++程序中为36字节的变量。当用StructTest3去接数据时,生成的结构体为一40字节(4+4+8+8*3)的对象,int变量A和double数组C接收正常,但long类型的B就错误:经.Net对齐操作后源数据会在StructTestAPI.Y 的内存后添加4个补齐字节,用StructTest3去接时,long类型的B字段会接到源char数组的后四位及4个补齐字节,自然不正确了。

当换用StructTest4去接数据时就正常了,它不会接到4个补齐字节。注意:这里B用的是byte[]而非char[] (C++中char为1个字节而C#中char为2个字节)。其实StructTest4里将B的SizeConst设置为12时,程序也能正常接到数据不会报错、异常,观察内存就可以知道其将4个补齐字节也接到了。

特例

结构体对齐操作的最大成员长度的原则对decimal类型并不适用。

public struct StructTest5
{
    public int A;
    public decimal B;
    public double C;
}
StructTest5 st5 = new StructTest5();
var s5 = Marshal.SizeOf(st5);//32

StructTest5的大小并不是按照之前对齐操作后的48的字节所料,而是32个字节。这是因为C#中decimal是由4个int构成的一个结构体变量,并不是单纯的16字节的变量。

struct Decimal
{
    private int flags;
    private int hi;
    private int mid;
    private int lo;    
}

StructTest5里的最大成员是double,对齐长度为8。