论坛上经常有人问某个结构体的大小为什么与他预计的不一致, 对特定问题,我肯定能回答出来; 但要从总体上说出个子丑寅卯, 我就不行了. 今日闲来无事, 特地研究了一下, 总算将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的对应关系应该是:

N sizeof(A) sizeof(B)
1 9 9
2 10 10
4 12 12
8 16 16
16 16 16

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字长, 但由于不能直接从定义中看出来, 你数的时候一定不要把它忘了; 还有就是虚拟继承等情况, 不过它们的结果与编译器实现关联太严重, 并且通常也碰不到, 这里就不做讨论了.