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。