结构体是C语言中一种复合数据类型,它允许我们将不同类型的数据元素组合在一起。这篇博客将介绍结构体类型,结构体变量的创建和初始化,以及结构体中存在的内存对齐问题。
一、什么是结构体?
结构体是一种用户定义的数据类型,它允许我们存储不同类型的数据项。结构体的定义使用struct
关键字,后面跟着结构体的名称和包含在大括号{}
中的数据项。每个数据项(也称为成员)都有一个名称和一个类型。
struct Student {
char name[50];
int roll;
float marks;
};
在上述代码中,我们定义了一个名为Student
的结构体,它有三个成员:name
,roll
和marks
。
二、创建和初始化结构体变量
我们可以像创建普通变量一样创建结构体变量。下面是一个例子:
struct Student s1;
在这里,s1
是一个Student
类型的变量。我们可以通过.
运算符访问结构体的成员,如s1.name
,s1.roll
和s1.marks
。
结构体变量也可以在声明时初始化。例如:
struct Student s1 = {"Tom", 12, 89.5};
三、结构体的内存对齐
在结构体中,每个成员都有一个与其类型相对应的自然对齐值。例如,int
类型的成员的自然对齐值通常为4,char
类型的成员的自然对齐值为1。编译器会根据这些自然对齐值来决定在结构体中为每个成员分配多少内存。
要弄清楚结构体内存是如何对齐的,首先要明白结构体的对齐规则。
- 结构体的第一个成员对齐到和结构体变量起始位置的偏移量为0的地址处
- 其他成员变量要对齐到和结构体变量起始位置的偏移量为对齐数的整数倍的地址处
【注】
(1)对齐数 = 编译器默认的一个对齐数(VS中的默认值为8) 与 该成员变量大小 中的 较小值
(2)Linux中gcc没有默认对齐数,对齐数就是成员变量自身大小- 结构体的总体大小为最大对齐数的整数倍
【注】
(1)结构体中每个成员变量都有一个对齐数,最大对齐数就是所有对齐数里最大的
(2)如果嵌套了结构体,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,此时结构体的总体大小为所有最大对齐数(包括嵌套的结构体成员的对齐数)的整数倍
考虑以下结构体示例:
#include <stdio.h>
int main()
{
struct EX1 {
char c;
short s;
int i;
};
struct EX2 {
char c;
int i;
short s;
};
struct EX3 {
double d;
char c;
int i;
};
struct EX4 {
char c;
struct EX3 s3;
int i;
};
printf("%zd\n", sizeof(struct EX1));
printf("%zd\n", sizeof(struct EX2));
printf("%zd\n", sizeof(struct EX3));
printf("%zd\n", sizeof(struct EX4));
return 0;
}
1. 各个结构体成员对应的对齐数
EX1 | 成员变量的大小 | 编译器默认的对齐数 | 成员变量对应的对齐数 |
c | 1 | 8 | 1 |
s | 2 | 2 | |
i | 4 | 4 | |
EX2 | 成员变量的大小 | 编译器默认的对齐数 | 成员变量对应的对齐数 |
c | 1 | 8 | 1 |
i | 4 | 4 | |
s | 2 | 2 | |
EX3 | 成员变量的大小 | 编译器默认的对齐数 | 成员变量对应的对齐数 |
d | 8 | 8 | 8 |
c | 1 | 1 | |
i | 4 | 4 | |
EX4 | 成员变量的大小 | 编译器默认的对齐数 | 成员变量对应的对齐数 |
c | 1 | 8 | 1 |
s3 | 自己成员中最大对齐数:8 | ||
i | 4 | 8 | 4 |
2. 内存对齐可视化详图
先以EX2为例:
- c作为该结构体第一个成员,对齐到偏移量为0的地址处。
- i的对齐数为4,应该从0开始寻找是4整数倍的偏移量,即4,从该地址处存入。
- 存入i后地址来到偏移量为8的位置,按顺序应该存入第三个成员s,它的对齐数为2,而该地址对应的偏移量8正好是2的整数倍,在此处存入。
- 从偏移量0处到9处,大小为10个字节,然而最大对齐数为4,需要追加2个字节直到为其整数倍,即最终的大小为12个字节。
再以EX4为例:
- c作为该结构体第一个成员,对齐到偏移量为0的地址处。
- s3作为嵌套结构体,应该对齐到其成员中最大对齐数(8)的整数倍处,向下寻找来到偏移量为8处存入。
- 存入s3后地址来到偏移量为24的位置,按顺序应该存入第三个成员i,它的对齐数为4,而该地址对应的偏移量24正好是4的整数倍,在此处存入。
- 从偏移量0处到27处,大小为28个字节,然而最大对齐数为8,需要追加4个字节直到为其整数倍,即最终的大小为32个字节。
程序输出各结构体的大小结果如下,可以发现与上文分析一致:
3. 结构体对齐的实质
结构体的内存对齐是拿空间换取时间的做法。
为何存在结构体内存对齐?
原因主要有以下几点:
(1)平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的。某些硬件平台只能在某些特定地址处取某些特定类型的数据,否则可能会抛出硬件异常。为了保证程序在不同平台之间的可移植性,需要进行内存对齐。(2)性能优化
访问未对齐的数据可能需要额外的CPU周期来处理跨边界的数据读取,这会降低程序执行效率。通过内存对齐,可以确保每个成员变量的地址都是其大小的整数倍,这样就可以直接通过单次内存访问操作获取整个数据,从而提高访问速度。(3)指令集要求
一些处理器的指令集设计就要求数据必须按照一定的字节边界对齐。例如,许多RISC架构的处理器要求访问内存时,地址必须是对齐的,否则会导致错误或效率下降。(4)缓存友好的数据布局
对齐后的数据在内存中更有可能以连续的方式存储,这对于利用现代处理器的高速缓存系统来说是非常重要的。因为高速缓存通常是以缓存行(cache line)为单位进行数据读取和写入的,对齐的数据更容易填充完整的缓存行,从而减少缓存未命中的情况。(5)满足编译器和操作系统的要求
许多编译器和操作系统都有默认的内存对齐策略,如果程序员没有显式指定内存对齐方式,编译器会自动进行对齐。因此,了解并控制内存对齐也是程序员在编写高效代码时需要注意的问题之一。综上所述,结构体内存对齐是一种性能优化手段,它能够提升数据访问速度、保持程序的可移植性,并适应各种硬件和软件环境的要求。
在设计结构体的时候,既要满足内存对齐,又要节省空间,相信聪明的你已经想到了应该如何做,那便是让占用空间小的成员尽量集中在一起。
结语
结构体是C语言中非常有用的一种数据类型,它允许我们将不同类型的数据组合在一起。理解结构体的内存对齐行为可以帮助我们更好地理解C语言如何在内存中布局数据,以及为什么某些结构体比我们预期的要大。