21-C语言的结构体尺寸——地址对齐问题
文章目录
- 21-C语言的结构体尺寸——地址对齐问题
- 一、CPU 字长
- 二、 地址对齐
- 2.1 原理和原因
- 2.2 地址对齐的主要思想
- 2.3 示例代码说明地址对齐
- 三、普通变量的M值
- M值的计算规则
- 例子
- 四、手动干预M值
- 4.1 规则
- 4.2 例子
- 五、结构体的M值
- 5.1 例子分析
- 5.1.1 分析步骤
- 5.1.2 填充和对齐
- 5.2 结构体大小计算
- 六、结构体得可移植性:统一结构体大小的策略
- 6.1 方法1:使用`__attribute__((packed))`进行压实
- 6.2 方法2:对每一个成员进行单独对齐设置
- 6.3 示例代码
一、CPU 字长
字长是指处理器在执行一条指令时所能处理的最大数据位数,主要影响处理器的运算能力和内存寻址能力。字长由处理器的设计和所使用的系统位数共同决定。
- 32位系统:每次最多可以处理32位(4字节)数据。
- 64位系统:每次最多可以处理64位(8字节)数据。
处理器的字长直接影响到计算机系统的性能和处理能力。64位处理器能够处理更多数据和地址空间,因此在运行大型应用程序和处理大数据集时具有优势。
二、 地址对齐
地址对齐是指在内存中存放数据时,将数据存放在地址能够被特定字长整除的位置上。地址对齐的主要目的是提高CPU访问内存的效率。
2.1 原理和原因
32位CPU的地址对齐:
- 存取内存策略:32位CPU每次存取内存数据时,最少存取4个字节(即32位)。
- 对齐方式:按4字节对齐,数据的起始地址通常是4的倍数。
- 高效读取数据:为了提高读取数据的效率,编译器会尽量将变量放在一个4字节单元内。如果变量较大,无法放入一个4字节单元,编译器则尽可能将变量放入两个4字节单元内。总的来说,使用最少的单元来存放数据是编译器的优化策略。
- 节省时间:通过对齐方式存放数据,可以减少CPU访问内存的次数,从而提高执行效率。
2.2 地址对齐的主要思想
- 提高CPU运行效率:通过减少CPU读取和写入内存的次数来提高运行效率。
- 内存单元使用:一个数据尽可能使用一个单元来存放,避免使用多个单元。两个单元能放下的数据绝不用三个单元。这种优化策略是为了减少内存访问时间,提高系统整体性能。
2.3 示例代码说明地址对齐
#include <stdio.h>
struct Example {
char a;
int b;
short c;
};
int main() {
struct Example example;
printf("Size of struct Example: %lu\n", sizeof(example));
printf("Address of a: %p\n", (void*)&example.a);
printf("Address of b: %p\n", (void*)&example.b);
printf("Address of c: %p\n", (void*)&example.c);
return 0;
}
在这个示例中,结构体 Example
包含三个成员:char a
、int b
和 short c
。编译器会自动对齐这些成员,以提高内存访问效率。具体对齐方式取决于编译器和处理器的对齐规则。
解释:
sizeof(example)
可能大于所有成员大小之和,这是由于编译器插入了填充字节来对齐数据。example.a
的地址通常是0x0
开始(相对结构体起始地址)。example.b
的地址通常是4的倍数。example.c
的地址通常是2的倍数,但在int
后面也会对齐到4
的倍数。
三、普通变量的M值
概念:一个数据的大小是固定的(例如,整型),如果这个数据所存放的地址能够被某个数整除(如4),那么这个数就称为该数据的M值。M值与系统的字长和数据大小有关
。
M值的计算规则
可以根据具体的系统字长和数据的大小来估算M值得大小.
例如:
- 整型变量
int i
- 占用 4 字节
- 如果
i
存放在能被4整除的地址下,则其地址对齐,M值为4
- 字符变量
char c
- 占用 1 字节
- 如果
c
存放在能被1整除的地址下,则其地址对齐,M值为1
- 短整型变量
short s
- 占用 2 字节
- 如果
s
存放在能被2整除的地址下,则其地址对齐,M值为2
- 双精度浮点变量
double d
- 占用 8 字节
- 如果
d
存放在能被4整除的地址下,则其地址对齐,M值为4(注意:在某些系统中,双精度浮点数的对齐要求可能为8)
- 浮点变量
float f
- 占用 4 字节
- 如果
f
存放在能被4整除的地址下,则其地址对齐,M值为4
例子
假设有以下变量的地址:
int i; // i 占用 4 字节
char c; // c 占用 1 字节
short s; // s 占用 2 字节
double d; // d 占用 8 字节
float f; // f 占用 4 字节
printf("i: %p\n", (void*)&i);
printf("s: %p\n", (void*)&s);
printf("c: %p\n", (void*)&c);
printf("d: %p\n", (void*)&d);
printf("f: %p\n", (void*)&f);
假设输出地址如下:
i: 0x7fffc900a298
s: 0x7fffc900a296
c: 0x7fffc900a295
d: 0x7fffc900a2a0
f: 0x7fffc900a29c
i
的地址0x7fffc900a298
是4的倍数,M值为4。s
的地址0x7fffc900a296
是2的倍数,M值为2。c
的地址0x7fffc900a295
是1的倍数,M值为1。d
的地址0x7fffc900a2a0
是4的倍数,M值为4(假设系统对齐要求为4)。f
的地址0x7fffc900a29c
是4的倍数,M值为4。
四、手动干预M值
可以使用 __attribute__((aligned(n)))
来手动指定变量的对齐方式。这是GNU特定的语法,属于C语言标准的扩展。
char c __attribute__((aligned(16)));
4.1 规则
__attribute__
前后都有两个下划线。__attribute__
右边由两对小括号(( ))
。__attribute__
还支持其他设置。
注意事项:
- 提升不降低:一个变量的M值只能提升,不能降低。即,不能手动设置对齐方式使其小于编译器默认的对齐方式。
- 2的幂次:M值必须是2的幂次,如1, 2, 4, 8, 16, 32, 64等。
4.2 例子
#include <stdio.h>
int main() {
// 定义变量
int i; // 默认对齐,M值为4
char c __attribute__((aligned(16))); // 强制对齐16字节,M值为16
// 打印地址
printf("Address of i: %p\n", (void*)&i);
printf("Address of c: %p\n", (void*)&c);
return 0;
}
在这个例子中,c
被强制对齐到16字节,这意味着其地址将是16的倍数,而 i
则默认对齐4字节。编译器会在生成的二进制文件中添加适当的填充字节以满足这些对齐要求。
五、结构体的M值
结构体的M值取决于其成员中M值最大的成员。为了确保结构体的地址对齐和整体大小满足对齐要求,需要考虑以下几点:
- 结构体的M值:结构体中有多个成员,取决于成员中
M值最大
的成员。 - 结构体的地址对齐:结构体的地址必须能被结构体的M值整除。
- 结构体的尺寸:结构体的大小等于成员中宽度最宽的成员的倍数。
5.1 例子分析
typedef struct node {
int i; // 4 字节
char c; // 1 字节
short s; // 2 字节
double d; // 8 字节
long double ld; // 16 字节
float f; // 4 字节
} Node;
5.1.1 分析步骤
-
成员变量的M值:
int i
的M值为4。char c
的M值为1。short s
的M值为2。double d
的M值为8。long double ld
的M值为16。float f
的M值为4。
-
结构体的M值:
- 取决于成员中M值最大的成员,即
long double ld
的M值,为16。 - 所以,结构体
Node
的M值为16。
- 取决于成员中M值最大的成员,即
-
成员变量的对齐:
int i
:占用4字节,对齐4字节。char c
:占用1字节,对齐1字节(紧接在i
后面,需要考虑填充)。short s
:占用2字节,对齐2字节(紧接在c
后面,需要考虑填充)。double d
:占用8字节,对齐8字节(紧接在s
后面)。long double ld
:占用16字节,对齐16字节。float f
:占用4字节,对齐4字节。
5.1.2 填充和对齐
假设在64位系统中,编译器的默认对齐方式为8字节:
int i
:4字节,占用地址偏移0到3。char c
:1字节,占用地址偏移4,填充3字节使short s
对齐。short s
:2字节,占用地址偏移8到9,填充6字节使double d
对齐。double d
:8字节,占用地址偏移16到23。long double ld
:16字节,占用地址偏移24到39。float f
:4字节,占用地址偏移40到43,填充4字节使结构体大小为48字节(最宽成员倍数)。
5.2 结构体大小计算
int i
:4字节,占用地址偏移0-3。char c
:1字节,占用地址偏移4。- 填充:3字节,占用地址偏移5-7。
short s
:2字节,占用地址偏移8-9。- 填充:6字节,占用地址偏移10-15。
double d
:8字节,占用地址偏移16-23。long double ld
:16字节,占用地址偏移24-39。float f
:4字节,占用地址偏移40-43。- 填充:4字节,占用地址偏移44-47。
结果:
在64位系统中,该结构体的大小为48字节,能够被long double
的对齐要求整除(16字节)。
代码示例:
#include <stdio.h>
typedef struct node {
int i;
char c;
short s;
double d;
long double ld;
float f;
} Node;
int main() {
Node node_instance;
printf("Size of struct Node: %lu\n", sizeof(Node));
printf("Address of i: %p\n", (void*)&node_instance.i);
printf("Address of c: %p\n", (void*)&node_instance.c);
printf("Address of s: %p\n", (void*)&node_instance.s);
printf("Address of d: %p\n", (void*)&node_instance.d);
printf("Address of ld: %p\n", (void*)&node_instance.ld);
printf("Address of f: %p\n", (void*)&node_instance.f);
return 0;
}
这段代码会打印出结构体Node
的大小和各成员变量的地址,通过这些地址可以验证结构体的对齐和填充。
六、结构体得可移植性:统一结构体大小的策略
为了确保结构体在不同操作系统(不同位数)上的大小保持一致,可以采用以下两种方法:
6.1 方法1:使用__attribute__((packed))
进行压实
通过使用__attribute__((packed))
,可以使编译器不在成员之间填充任何空隙,确保结构体的每个成员紧密排列。
typedef struct node {
int i;
char c;
short s;
double d;
long double ld;
char kk;
float f;
} __attribute__((packed)) Node;
解释:
__attribute__((packed))
:告诉编译器不要在成员之间插入任何填充字节。这样可以确保结构体在不同平台上的大小一致,但是可能会导致访问非对齐数据的性能下降。
6.2 方法2:对每一个成员进行单独对齐设置
通过对每个成员进行单独对齐设置,可以确保结构体在不同平台上的对齐方式一致。
struct node {
int i __attribute__((aligned(4)));
char c __attribute__((aligned(1)));
short s __attribute__((aligned(2)));
double d __attribute__((aligned(4)));
long double ld __attribute__((aligned(4)));
char kk __attribute__((aligned(1)));
float f __attribute__((aligned(4)));
};
解释:
__attribute__((aligned(n)))
:指定每个成员的对齐方式为n
字节。这可以确保结构体成员在不同平台上的对齐方式一致,从而确保结构体大小一致。
注意事项:
- 结构体的大小:取决于多个因素,包括地址对齐、M值(最大对齐值)等。默认情况下,结构体的大小为成员中最大M值的倍数。
- 可移植性:为了实现结构体的可移植性,需要使用可移植类型并结合
__attribute__
机制对结构体进行压实。 - 性能:使用
__attribute__((packed))
可能会导致访问非对齐数据的性能下降,具体影响取决于具体平台。
6.3 示例代码
以下示例展示了如何使用两种方法创建可移植的结构体,并验证其大小和成员偏移:
#include <stdio.h>
typedef struct node_packed {
int i;
char c;
short s;
double d;
long double ld;
char kk;
float f;
} __attribute__((packed)) NodePacked;
struct node_aligned {
int i __attribute__((aligned(4)));
char c __attribute__((aligned(1)));
short s __attribute__((aligned(2)));
double d __attribute__((aligned(4)));
long double ld __attribute__((aligned(4)));
char kk __attribute__((aligned(1)));
float f __attribute__((aligned(4)));
};
int main() {
NodePacked packed_instance;
struct node_aligned aligned_instance;
printf("Size of packed struct: %lu\n", sizeof(NodePacked));
printf("Size of aligned struct: %lu\n", sizeof(struct node_aligned));
printf("Offsets in packed struct:\n");
printf("Offset of i: %lu\n", offsetof(NodePacked, i));
printf("Offset of c: %lu\n", offsetof(NodePacked, c));
printf("Offset of s: %lu\n", offsetof(NodePacked, s));
printf("Offset of d: %lu\n", offsetof(NodePacked, d));
printf("Offset of ld: %lu\n", offsetof(NodePacked, ld));
printf("Offset of kk: %lu\n", offsetof(NodePacked, kk));
printf("Offset of f: %lu\n", offsetof(NodePacked, f));
printf("Offsets in aligned struct:\n");
printf("Offset of i: %lu\n", offsetof(struct node_aligned, i));
printf("Offset of c: %lu\n", offsetof(struct node_aligned, c));
printf("Offset of s: %lu\n", offsetof(struct node_aligned, s));
printf("Offset of d: %lu\n", offsetof(struct node_aligned, d));
printf("Offset of ld: %lu\n", offsetof(struct node_aligned, ld));
printf("Offset of kk: %lu\n", offsetof(struct node_aligned, kk));
printf("Offset of f: %lu\n", offsetof(struct node_aligned, f));
return 0;
}