C 语言 sizeof(结构体) 到底有多大

C 语言中各个数据类型的大小

类型 大小 范围
char 1 字节 -128 到 127 或 0 到 255
unsigned char 1 字节 0 到 255
signed char 1 字节 -128 到 127
int 2 或 4 字节 -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647
unsigned int 2 或 4 字节 0 到 65,535 或 0 到 4,294,967,295
short 2 字节 -32,768 到 32,767
unsigned short 2 字节 0 到 65,535
long 4 字节 -2,147,483,648 到 2,147,483,647
unsigned long 4 字节 0 到 4,294,967,295
结构体 (struct) 待分析,需要考虑字节对齐
联合 (union) 所有成员中最长的
枚举 (enum) 根据数据类型

单层结构体大小

如果结构体中的成员数据类型相同,这样的情况最简单,结构体大小=数据类型*数据个数

#include <stdio.h>
typedef struct Test1
{
    int a;
    int b;
} T1;

typedef struct Test2
{
    char a;
    char b;
} T2;

int main()
{
    T1 t1;
    int siz01 = sizeof(t1);
    printf("%d\n", siz01); //8

    T2 t2;
    int siz02 = sizeof(t2);
    printf("%d\n", siz02); //2
    return 0;
}

但是结构体中通常数据类型都各不相同,成员按照定义时的顺序依次存储在连续的内存空间。和数组不一样的是,结构体的大小不是所有成员大小简单的相加,需要考虑到地址对齐问题。看下面这样的一个结构体:

#include <stdio.h>
typedef struct Test3
{
    int a;
    char b;
    int c;
} T3;

int main()
{
    T3 t3;
    int siz03 = sizeof(t3);
    printf("t3: %d\n", siz03); //t3: 12
    return 0;
}

sizeof求该结构体的大小,发现值为12int4个字节,char1个字节,结果应该是9个字节才对啊,为什么呢?

先介绍一个相关的概念——偏移量。偏移量指的是结构体变量中成员的地址和结构体变量地址的差。结构体大小等于最后一个成员的偏移量加上最后一个成员的大小。显然,结构体变量中第一个成员的地址就是结构体变量的首地址。因此,第一个成员int a的偏移量为0。第二个成员char b的偏移量是第一个成员的偏移量加上第一个成员的大小0+4,其值为4;第三个成员int c的偏移量是第二个成员的偏移量加上第二个成员的大小4+1,其值为5

即结构体的大小等于最后一个成员变量的地址与第一个成员变量的地址之差,再加上最后一个成员变量的大小。

如果不考虑对齐的情况,变量在内存中的存放如下,

//t3
    ________
0  |aaaabccc| 7
8  |c       | 15
    ‾‾‾‾‾‾‾‾

当我们凭直觉去用4+1+4=9来计算结构体大小时并不会觉得有什么错,但是通过内存的排放可以直观的看到,第三个变量的存放有点奇怪。CPU 从内存中读取肯定也是极为不便的。实际存储变量时,地址要求对齐的。编译器在编译程序时会遵循两条原则:

  • 结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)
  • 结构体大小必须是所有成员大小的整数倍,也即所有成员大小的公倍数。

我们在回头分析上述的例子,前两个成员的偏移量0 4都满足要求,但第三个成员的偏移量为5,并不是自身int大小的整数倍。编译器在处理时会在第二个成员后面补上3个空字节,使得第三个成员的偏移量变成8。结构体大小等于最后一个成员的偏移量加上其大小,上面的例子中计算出来的大小为12,满足公倍数要求。

直观描述这个结构体在内存中的存储如下,星号*表示该段内存因为内存对齐被占用,也就是其实际大小。字母个数表示其单独拿出来的大小

//t3
    ________
0  |aaaab***| 7
8  |cccc    | 15
    ‾‾‾‾‾‾‾‾

再看一例,

#include <stdio.h>
typedef struct Test4
{
    int a;
    short b;
} T4;
int main()
{
    T4 t4;
    int siz04 = sizeof(t4);
    printf("t4: %d\n", siz04); //t4: 8
    return 0;
}

成员int a的偏移量为 0;成员short b的偏移量为 4,都不需要调整。但计算出来的大小为6,显然不是成员int a大小的整数倍。因此,编译器会在成员int b后面补上2个字节,使得结构体的大小变成8从而满足第二个公倍数要求。

由此可见,结构体类型需要考虑到字节对齐的情况,不同的顺序会影响结构体的大小

#include <stdio.h>
typedef struct Test5
{
    char a;
    int b;
    char c;
} T5;
typedef struct Test6
{
    char a;
    char b;
    int c;
} T6;
int main()
{
    T5 t5;
    int siz05 = sizeof(t5);
    printf("t5: %d\n", siz05); //t5: 12

    T4 t6;
    int siz06 = sizeof(t6);
    printf("t6: %d\n", siz06); //t6: 8
    return 0;
}

两个结构体成员都一样,但是一个大小为12一个大小为8。我们将其在内存的存储画出来就可以明白,

//t5
    ________
0  |a***bbbb| 7
8  |c***    | 15
    ‾‾‾‾‾‾‾‾
//t6
    ________
0  |ab**cccc| 7
    ‾‾‾‾‾‾‾‾

总结:

  • 结构体大小等于最后一个成员的偏移量加上最后一个成员的大小
  • 结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)
  • 结构体大小必须是所有成员大小的整数倍,也即所有成员大小的公倍数
  • 不同的顺序会影响结构体的大小

嵌套结构体大小

对于嵌套的结构体,需要将其展开。对结构体求sizeof时,上述两种原则变为:

  • 展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍。
  • 结构体大小必须是所有成员大小的整数倍,这里所有成员计算的是展开后的成员,而不是将嵌套的结构体当做一个整体。
#include <stdio.h>
typedef struct Test7
{
    short a;
    struct
    {
        char b;
        int c;
    } tt;
    int d;
} T7;
int main()
{
    T7 t7;
    int siz07 = sizeof(t7);
    printf("t7: %d\n", siz07); //t7: 16
    return 0;
}

根据原则一,tt的偏移量应该是4,而不是2

在内存中的存储,

//t7
    ________
0  |aa**b***| 7
8  |ccccdddd| 15
    ‾‾‾‾‾‾‾‾
#include <stdio.h>
typedef struct Test8
{
    char a;
    struct
    {
        char b;
        int c;
    } tt;
    char d;
    char e;
    char f;
    char g;
    char h;
} T8;

int main()
{
    T8 t8;
    int siz08 = sizeof(t8);
    printf("t8: %d\n", siz08); //t8: 20
    return 0;
}

结构体tt单独计算占用空间为8,而t8则是20,不是8的整数倍,这说明在计算sizeof(t8)时,将嵌套的结构体tt展开了,这样t8中最大的成员为tt.c,占用 4 个字节,20为 4 的整数倍。如果将tt当做一个整体,结果应该是24了。

在内存中的存储,

//t8
    ________
0  |a***b***| 7
8  |ccccdefg| 15
16 |h***    | 31
    ‾‾‾‾‾‾‾‾

另一个特殊的例子是结构体中包含数组,其大小计算应当和处理嵌套结构体一样,将其展开,如下例子:

#include <stdio.h>
typedef struct Test9
{
    char a;
    float b;
    int c[2];
} T9;
int main()
{
    T9 t9;
    int siz09 = sizeof(t9);
    printf("t9: %d\n", siz09); //t9: 16
    return 0;
}

char a占一个字节,偏移量为0short b占四字节,偏移量为2,不是最大成员的整数倍,这里取最大成员是int或者short的大小的倍数。而不是整个数组int c[2]的倍数。所以short b偏移量扩展为4

内存中存储,

//t9
    ________
0  |a***bbbb| 7
8  |cccccccc| 15
    ‾‾‾‾‾‾‾‾

总结:

  • 展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍。
  • 结构体大小必须是所有成员大小的整数倍,这里所有成员计算的是展开后的成员,而不是将嵌套的结构体当做一个整体。
  • 想象在内存中的存储,保证对齐要求,基本上可以比较准确的算出来