为什么具有相同字段的结构体A和结构体B的大小不相等?

11

为什么struct A的大小不等于struct B的大小?

我需要做什么来让它们的大小相同吗?

using System;

namespace ConsoleApplication1
{
    class Program
    {
        struct A
        {
            char a;
            char c;
            int b;
        }

        struct B
        {
            char a;
            int b;
            char c;

        }


        static void Main(string[] args)
        {
            unsafe
            {
                Console.WriteLine(sizeof(A));
                Console.WriteLine(sizeof(B));
            }
            Console.ReadLine();
        }
    }
}

输出结果为:

8
12

这是由于填充规则导致的。相关的SO链接:https://dev59.com/PG855IYBdhLWcg3wfUjQ - Nicholas Smith
3
可能是与C#中结构体的sizeof运算符返回额外大小的问题重复。 - Raymond Chen
4个回答

9

字段之间有一些填充。填充使用前一个字段和下一个字段计算。

此外,该条件应为真:

(size of struct) % (size of largest type) == 0

在您的情况下,最大的类型是int,其大小为4字节。
struct A
{
    char a; // size is 2, no previous field, next field size is 2 - no alignment needed
    char c; // size is 2, previous size is 2 -> 2 + 2 = 4, next size is 4 - no alignment needed
    int b;  //size is 4, it is last field, size is 4 + 4 = 8.  

    //current size is 2 + 2 + 4 = 8
    //8 % 4 == 0 - true - 8 is final size
}

struct B
{
    char a; // size is 2, next size is 4, alignment needed - 2 -> 4, size of this field with alignment is 4
    int b;  // size is 4, previous is 4, next size is 2(lower) - no alignment needed
    char c; // size is 2, previous is 4 + 4 = 8 - no alignment needed

    //current size is 4 + 4 + 2 = 10
    //but size should be size % 4 = 0 -> 10 % 4 == 0 - false, adjust to 12
}

如果你想让两个结构体具有相同的大小,可以使用LayoutKind.Explicit
[StructLayout(LayoutKind.Explicit)]
public struct A
{
    [FieldOffset(0)]
    char a;

    [FieldOffset(2)]
    char c;

    [FieldOffset(4)]
    int b;
}

[StructLayout(LayoutKind.Explicit)]
public struct B
{
    [FieldOffset(0)]
    char a;

    [FieldOffset(2)]
    int b;

    [FieldOffset(6)]
    char c;
}

或者

你可以使用 LayoutKind.SequentialPack = 1CharSet = CharSet.Unicode 来获得大小为8。

[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)]
public struct A
{
    char a;
    char c;
    int b;
}

[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)]
public struct B
{        
    char a;
    int b;
    char c;
}

此外,您可以在不使用unsafe的情况下获取结构大小:
Console.WriteLine(System.Runtime.InteropServices.Marshal.SizeOf(typeof(A)));
Console.WriteLine(System.Runtime.InteropServices.Marshal.SizeOf(typeof(B)));

@seyxsultan,我不知道什么是最好的方法,但使用 LayoutKind.Sequential 会得到不同的大小。 - Roman
@seyxsultan,你得到了哪个尺寸?如果你得到的是6号尺码,那么这个尺码是错误的。因为你有两个char变量,每个变量占用2个字节,还有一个int变量,大小为2+2+4=8 - Roman
我得到了8,对于两个 - user7479343
@seyxsultan, 我得到了 size == 6,但无论如何,如果它对你有用的话就很好了,感谢你的回复。 - Roman
@seyxsultan,请尝试使用Console.WriteLine(System.Runtime.InteropServices.Marshal.SizeOf(typeof(A)));打印大小。 - Roman
显示剩余3条评论

7
这是因为您的编译器保留在struct的成员之间插入填充以及在末尾添加一些空间的权利。(但请注意,在第一个成员之前不允许插入填充。)
它这样做是为了使成员的起始位置对齐在易于寻址的内存位置上。
特别地,编译器可能会在单个charint之间插入填充。因此,由偶数个char后跟一个int组成的结构可能比由一个char、一个int,后面跟奇数个char组成的结构占用更少的空间。

我认为这取决于特定的C#实现。(值得一提的是,尽管这些功能不是这些语言的标准要求,但在许多编译器中,C和C++都有相应的功能。)也许可以将其作为一个更具针对性的问题来询问? - Bathsheba

6
这是处理器实现的细节问题,.NET非常努力地隐藏了这一点。变量需要具有存储位置,以便处理器可以通过单个数据总线操作读取和写入值。这使得变量地址的对齐非常重要。读取单个字节从来不是问题。但是短整型(2个字节)应该具有2的倍数的地址。整型(4个字节)应该具有4的倍数的地址。理想情况下,长整型或双精度浮点型(8个字节)应该具有8的倍数的地址,但在32位处理器上并不总是可行。
英特尔和AMD处理器允许未对齐的读取和写入,而RISC内核则不允许。但是这可能会带来成本,可能需要两个数据总线周期来读取两个字节块,一个值的上半部分和下半部分的一部分字节。使用将这些字节移动到正确位置的电路。这需要时间,通常需要额外的1到3个时钟周期。在RISC内核上处理总线错误陷阱需要大量的时间。
但更严重的是,它破坏了.NET内存模型。它为简单值类型和对象引用提供原子性保证。未对齐的读取和写入会破坏该承诺。它可能会导致“撕裂”,观察到正在写入的字节的一部分。而且更糟糕的是,它可能会破坏垃圾收集器。GC在对象引用被原子更新时大量依赖。
因此,当CLR确定结构或类的布局时,它必须确保满足对齐要求。如果没有,则需要在变量之间留下额外的未使用空间。并且可能需要额外的空间来确保成员在存储在数组中时仍然对齐。这个额外空间的通用术语是“填充”。
针对类声明,它具有[StructLayout(LayoutKind.Auto)],它可以重新排列成员以实现最佳布局。不是结构体,默认情况下它们是LayoutKind.Sequential。除了类和结构体之外,静态变量以及方法的参数和局部变量也需要此对齐保证。但不太容易观察到。

0
字段的顺序不同;我猜成员的大小也不同,因为它们被填充(即以这样的方式定位,使它们从机器字的偶数位置开始,以便在内存消耗的代价下更容易访问)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接