sizeof 的计算

论坛上经常有人问某个结构体的大小为什么与他预计的不一致,对特定问题,我肯定能回答出来;但要从总体上说出个子丑寅卯,我就不行了。今日闲来无事,特地研究了一下,总算将 sizeof 的计算方法大概梳理清楚了。

字节对齐

每种 CPU 都有其特定的字长,如目前最常见的 32 位 CPU,其字长就是 32 位(4字节),即将普及的 64 位 CPU,字长就是 64 位(8字节)。而 CPU 对内存的访问总是从字长的整数倍开始,以字长为单位的。例如(假设是 32 位 CPU,下同)读一个 32 位整数,如果其起始地址是 0,则只需一次内存操作;但如果起始地址是 1、2 或 3,则需要两次操作来将相关的两个字都读出来,再在 CPU 内部进行处理才行。对 x86 CPU,这样做仅会稍微降低一点效率,但如果是某些其它类型的 CPU 则可能会有成千上万倍的效率损失,甚至是程序根本无法运行。

从以上的分析可以看出,我们定义结构体时,为了提高效率和可移植性,应该尽量让 CPU 以最少的内存操作访问其任意一个成员。例如对于第一个成员是 char 型,第二个成员是 int 型的结构,其体积应该是 5 字节,但显然在这种情况下 CPU 对 int 型成员的访问要两次内存操作,而不是最少的一次。所以我们应该浪费一些空间,在 char 型成员后面加上 3 个填充字节,使 int 型成员的起始位置对齐 CPU 字长的整数倍。

不过,一个像回事的程序会有大量结构定义,如果都去人为计算,手工填充,工作量就太大了。因此,编译器特意设置了一个选项(在 vs2005 中,它是 项目属性 | c/c++ | code generation | struct member alignment)来配置默认的字节对齐方式,你可以选择 1、2、4、8 或者 16 字节对齐。如果你什么也没有选的话,它的默认值是 8。当然,编译器也并没有搞一刀切,对于需要特殊处理的结构,你还可以使用 #pragma pack 编译指令单独为其指定其合适的对齐方式。

注意,虽然我们从 CPU 字长引出了编译器的字节对齐,但它们是两个不同的概念,一定不要弄混。从某种意义上,你可以把字节对齐理解为:我的程序将运行在字长是这个数值的 CPU 上。

简单结构

这里所说的简单结构,是指仅由编译器内置类型构成的结构。我们已经知道,计算其体积,需要其字节对齐设置,这里假设其值是 N 则(有点拗口,多读几遍):

简单结构的大小,是其符合“使 CPU 在任意情况下,访问其任意成员的内存操作次数不大于 (sizeof(此成员) + N - 1)/N(这里的除法使用的是 C/C++ 整数除法的语意)”的所有内存布局中,体积最小的那个占用的字节数。

例如结构:

struct A
{
    char c;
    double d;
};

struct B
{
    double d;
    char c;
};

其 sizeof 的结果与 N 的对应关系应该是:

Nsizeof(A)sizeof(B)
199
21010
41212
81616
161616

A 的结果我想大家都能计算出来,就不多解释了。但你可能会问:当 N>1 时,B 的结果为什么不是 9 了呢?它是 9 才是符合你上面的描述呀? 这里你就要注意我所说的“任意情况”了,如果我们总是使用单个的 B,那 sizeof(B) 总是 9 肯定没问题,但问题是我们有时会定义一个 B 的数组,如 B array[2],这时,如果我们访问 array[1].d 会需要几次内存操作呢?不用说,它超过了 (sizeof(d) + N - 1)/N,所以它的大小不能总是 9。

复合结构

与简单结构相对,复合结构就是指那些部分或全部成员也是结构的结构。其实没有必要把它单独列出来的,因为其大小的计算方法与简单结构完全一样。唯一要注意的就是:对其每个成员的大小的计算要按定义这个成员的类型时的字节对齐来进行,而不能按定义这个复合结构时的对齐设置。例如:

#pragma pack( push, 8 ) 
struct A 
    char c; 
     double d; 
}; 
#pragma pack( 1 ) 
struct B 
    char c; 
     A a; 
}; 
#pragma pack( pop )

sizeof(A)sizeof(B),将分别是 16 和 17,而不是 16 和 10。

特殊情况

结构体的大小还有几种特殊情况,一是空结构,即没有任何成员的结构,这种情况,编译器规定其大小是 1;二是如果结构有虚函数,则它会包含一个虚函数表指针(vfptr,但这只是一般情况,具体取决与编译器实现),它的大小等于 CPU 字长,但由于不能直接从定义中看出来,你数的时候一定不要把它忘了;还有就是虚拟继承等情况,不过它们的结果与编译器实现关联太严重,并且通常也碰不到,这里就不做讨论了。