内存布局
一般的, 内存布局我们是不需要关心的, 因为我们直接通过字段或属性来访问结构体, 但是与非托管库操作的时候, 有时候就需要注意结构体布局了, 只有保证布局一致, 才能保证直接传结构体指针时, 非托管代码能正常访问到成员.
[StructLayout(LayoutKind.Sequential)] // 声明 StructLayout
struct MyStruct
{ }
序列布局 (Sequential)
顺序布局就是按照你在结构体中声明成员的顺序, 一个个将它们放到内存中, 不过需要注意的是, 这些成员不是一个个紧挨着的, 他们可能存在内存对齐, 不过这个我们下面会详细讲到.
显式布局 (Explicit)
在这种布局中, 你需要指定结构体中每一个字段在这个结构体中的偏移量, 例如你有一个结构体, 它里面有两个 int, 你希望这两个 int 之间隔开 2 字节的大小, 那么只需要为第一个结构体指定偏移量为 0, 第二个结构体偏移量为 6 即可.
自动布局 (Auto)
在这种布局中, 你不应该进行与非托管的互操作, 因为为了性能, 结构体中的成员顺序会被自动调整. 例如下面这个明显没办法在不调整顺序与不添加间隔的情况下做到内存对齐的结构体, 它的成员顺序, 会被调整.
[StructLayout(LayoutKind.Auto)]
struct SomeIntegers
{
byte AByte;
short AShortInteger;
byte AnotherByte;
// 你实际得到的可能是 byte, byte, short 这样的一个结构体
}
内存对齐
当你使用序列布局的时候, 结构体成员会有内存对齐现象, 而在进行内存对齐时, 会有以下行为:
- 一个成员的内存偏移量, 应该能够被它自身所占大小整除
- 如果一个成员占用内存大于包(Pack)的大小, 那么不再要求它的偏移量能被它自身大小整除, 而是能够被包大小整除即可.
什么是包
包就是内存对齐的要求大小, 例如在 Windows 中默认是 8 字节对齐, 像是一些大于八字节的数据, 按照 8 字节在内存中进行对齐即可.
偏移量要求
举个例子, 如果我们有一个 int(32位), 那么它的内存偏移量应该是 4, 8, 12 等这些能够被 4 整除的值, 同理, long(64位) 的偏移量也应该是 8, 16, 32 这些.
举个例子, 下面这个结构体中, 成员 B 为了实现偏移量为 2, 在成员 A 后产生了 1 字节的空隙.
[StructLayout(LayoutKind.Sequential)]
struct SomeIntegers
{
byte A; // 1 byte
// 1 byte
short B; // 2 bytes
}
成员占用大于包
举个例子, 在使用 8 字节的包大小时, 且在一个包内, 已经被使用了 4 字节, 如果你要装下一个 long(8字节), 那么显然这个包已经装不下这个字段了, 那么这个字段会放到下一个包.
举个例子, 下面这个结构体中, 成员 B 为了做到 8 字节的对齐, 它与第一个成员之间, 产生了 4 字节的空隙.
[StructLayout(LayoutKind.Sequential)]
struct SomeIntegers
{
int A; // 4 bytes
// 4 bytes
long B; // 8 bytes
}
但是当你指定 Pack 为 4 时, 这个 long 则不再要求偏移量能被 8 整除, 而是被 4 整除即可.
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct SomeIntegers
{
int A; // 4 bytes
long B; // 8 bytes (B 与 A 之间的空隙没有了)
}
因此, 当你不希望这个结构体产生任何空隙, 或者不希望这个结构体有内存对齐时, 指定 Pack = 1 就可以解决问题. 因为这样会导致所有字段的偏移量能被 1 整除即可, 于是他们对于偏移量, 就没有了任何要求.
成员尾部留空
一个结构体尾部也会产生一些空余的, 不被使用的字节, 这个字节大小取决于结构体中最大的成员大小.
例如我一个结构体中, 有一个 long
, 有一个 byte
, 最大成员大小为 8, 所以结构体的大小一定是 8 的倍数.
[StructLayout(LayoutKind.Sequential)]
struct TwoIntegers // 大小共计 16 bytes
{
long A; // 8 bytes
byte B; // 1 byte
// 7 bytes
}
当结构体嵌套
例如我一个结构体中包含另外一个结构体, 那么此时, 内存如何对齐呢?
- 结构体字段与前一个字段之间会产生的间隙, 取决于结构体中最大的对齐大小.
- 结构体字段自身所存在的尾部留空内存, 仍然会在外层结构体中保留
1. 结构体字段前的间隙
例如一个结构体中, 有一个 int
字段以及一个 byte
字段, 它的最大对其大小是 4, 也就是说, 这个结构体在作为其他结构体的成员时, 也会使用 4 作为对齐大小.
[StructLayout(LayoutKind.Sequential)]
struct SomeIntegers
{
public byte A; // 1 byte
// 3 bytes (结构体最大对齐是 4, 所以这里留出了 4 - 1 = 3 个字节)
public TwoIntegers B; // 8 bytes
public byte C; // 1 byte
// 3 bytes
}
[StructLayout(LayoutKind.Sequential)]
struct TwoIntegers
{
int A; // 4 bytes
byte B; // 1
}
2. 结构体尾部留空
即便结构体成员尾部的留空能够装下下一个成员, 它也不会这样做. “结构体自己的内存空间完整不可侵犯”
[StructLayout(LayoutKind.Sequential)]
struct SomeIntegers
{
public byte A; // 1 byte
// 3 byte
public TwoIntegers B; // 8 bytes
public byte C; // 1 byte (尽管上一个结构体字段后有留空, 但这段留空不会被重复利用)
// 3 bytes (所有成员的最大大小是 4, soyi这里留
}
[StructLayout(LayoutKind.Sequential)]
struct TwoIntegers
{
int A; // 4 bytes
byte B; // 1 byte
// 3 bytes
}
实现联合体
C++ 中有联合体这个东西, 实现多个字段共用一些数据, 在 C# 中, 如果你要实现这个, 使用显式布局即可.
举个例子, 在下面这个 C++ 定义的结构体中, 存在两个字段 A 和 B, 他们共用相同的内存区域.
struct SomeIntegers
{
union {
int A;
int B;
};
};
在 C# 中实现这个, 你可以使用:
[StructLayout(LayoutKind.Explicit)]
struct SomeIntegers
{
[FieldOffset(0)]
int A;
[FieldOffset(0)]
int B;
}
或者这样的 C++ 结构体:
struct SomeIntegers
{
union {
int A;
struct {
short Head;
short Tail;
};
};
};
可以这样用 C# 进行编写:
[StructLayout(LayoutKind.Explicit)]
struct SomeIntegers
{
[FieldOffset(0)]
int A; // 占 4 字节
[FieldOffset(0)]
short Head; // 占 2 字节
[FieldOffset(2)]
short Tail; // 占 2 字节
}