目录
前言
01-基本概念
02-示例说明
[2.1 计算过程分析:](#2.1 计算过程分析:)
[2.2 输出结果:](#2.2 输出结果:)
注意:
03-示例2
[3.1 示例结构体定义](#3.1 示例结构体定义)
详细计算过程
[3.2 输出结果](#3.2 输出结果)
[3.3 关键结论](#3.3 关键结论)
04-示例3
[4.1 结构体大小计算过程](#4.1 结构体大小计算过程)
分步计算:
[4.2 输出结果:](#4.2 输出结果:)
[4.3 内存布局示意图:](#4.3 内存布局示意图:)
[4.4 总结:](#4.4 总结:)
[4.5 优化](#4.5 优化)
[优化后结构体的内存布局分析(地址从 0 开始):](#优化后结构体的内存布局分析(地址从 0 开始):)
05-优化核心思路:
C语言\] 结构体 内存对齐规则 内存大小计算
## 前言
定义一个结构体,结构体中有 uint8 uint16 uint32的成员变量,怎么计算这个结构体在内存中的大小?请举个例子详细解释
⭐在 C 语言中,结构体的内存大小计算需要考虑**成员变量的大小** 和**内存对齐(Padding)** 两个因素,而不仅仅是简单地将所有成员大小相加。内存对齐是编译器为了提高访问效率而采取的策略,不同成员类型有不同的对齐要求。
## 01-基本概念
* 每个基本类型(如`uint8_t`、`uint16_t`、`uint32_t`)有其**自然对齐值** (通常等于其自身大小):
* `uint8_t`(1 字节):对齐值为 1
* `uint16_t`(2 字节):对齐值为 2
* `uint32_t`(4 字节):对齐值为 4
* 结构体的总大小必须是其**最大成员对齐值**的整数倍。⭐
## 02-示例说明
定义一个包含`uint8_t`、`uint16_t`、`uint32_t`的结构体,计算其内存大小:
#include
#include // 包含uint8_t、uint16_t、uint32_t的定义
// 定义结构体
struct MyStruct {
uint8_t a; // 1字节
uint16_t b; // 2字节
uint32_t c; // 4字节
};
int main() {
// 打印各成员大小
printf("uint8_t 大小: %zu 字节\n", sizeof(uint8_t));
printf("uint16_t 大小: %zu 字节\n", sizeof(uint16_t));
printf("uint32_t 大小: %zu 字节\n", sizeof(uint32_t));
// 打印结构体总大小
printf("结构体 MyStruct 大小: %zu 字节\n", sizeof(struct MyStruct));
return 0;
}
### 2.1 计算过程分析:
1. **成员`a`(`uint8_t`)**:
* 占 1 字节,从地址 0 开始存放(满足 1 字节对齐)。
* 此时已用内存:1 字节。
2. **成员`b`(`uint16_t`)**:
* 要求 2 字节对齐(地址必须是 2 的倍数)。
* 上一个成员结束于地址 1,不满足对齐要求,因此需要**填充 1 字节的空白**(地址 1 处)。
* `b`从地址 2 开始存放,占用地址 2-3(2 字节)。
* 此时已用内存:1(a) + 1(填充) + 2(b) = 4 字节。
3. **成员`c`(`uint32_t`)**:
* 要求 4 字节对齐(地址必须是 4 的倍数)。
* 上一个成员结束于地址 3,下一个地址 4 正好满足 4 字节对齐。
* `c`从地址 4 开始存放,占用地址 4-7(4 字节)。
* 此时已用内存:4 + 4(c) = 8 字节。
4. **结构体总大小**:
* 最大成员对齐值为 4(`uint32_t`),8 是 4 的整数倍,无需额外填充。
* 最终大小:8 字节。
### 2.2 输出结果:
uint8_t 大小: 1 字节
uint16_t 大小: 2 字节
uint32_t 大小: 4 字节
结构体 MyStruct 大小: 8 字节
#### 注意:
* 如果调整成员顺序,结构体大小可能改变。例如,将`uint32_t`放在最前:
struct MyStruct {
uint32_t c; // 4字节
uint16_t b; // 2字节
uint8_t a; // 1字节
};
* 此时总大小为 7 字节,但因需要满足 4 字节对齐,最终会填充 1 字节,总大小为 8 字节。
* 可以通过**#pragma pack**等编译器指令修改对齐方式,但不建议随意修改,可能影响性能。
从输出结果可以看到,这个结构体的总大小是 8 字节,而不是简单的 1+2+4=7 字节,这正是因为内存对齐机制导致的(在`uint8_t a`和`uint16_t b`之间填充了 1 字节的空白)。
我们可以通过打印成员的内存地址来更直观地看到对齐情况:
#include
#include
struct MyStruct {
uint8_t a;
uint16_t b;
uint32_t c;
};
int main() {
struct MyStruct s;
printf("a的地址: %p\n", &s.a);
printf("b的地址: %p\n", &s.b);
printf("c的地址: %p\n", &s.c);
return 0;
}
假设输出的地址是:
a的地址: 0x7ffd6b7a4a30
b的地址: 0x7ffd6b7a4a32 // 与a相差2字节(中间有1字节填充)
c的地址: 0x7ffd6b7a4a34 // 与b相差2字节(刚好放下b)
可以清晰地看到:
* `a`占用 1 字节(0x30)
* 地址 0x31 被填充(不存储有效数据)
* `b`从地址 0x32 开始(满足 2 字节对齐),占用 0x32-0x33
* `c`从地址 0x34 开始(满足 4 字节对齐),占用 0x34-0x37
整个结构体从 0x30 到 0x37,共 8 字节,刚好是最大成员(4 字节)的 2 倍,满足结构体整体对齐要求。
## 03-示例2
uint8_t 大小: 1 字节
uint16_t 大小: 2 字节
uint8_t 大小: 1 字节
uint16_t 大小: 2 字节
uint32_t 大小: 4 字节
我们来分析包含`uint8_t`、`uint16_t`、`uint8_t`、`uint16_t`、`uint32_t`这 5 个成员的结构体大小,关键依然是**内存对齐规则**。
### 3.1 示例结构体定义
#include
#include
struct MyStruct {
uint8_t a; // 1字节
uint16_t b; // 2字节
uint8_t c; // 1字节
uint16_t d; // 2字节
uint32_t e; // 4字节
};
int main() {
printf("结构体大小: %zu 字节\n", sizeof(struct MyStruct));
return 0;
}
#### 详细计算过程
详细计算过程(内存地址从 0 开始)
1. **成员`a`(`uint8_t`)**:
* 占 1 字节,地址 0(满足 1 字节对齐)
* 已用内存:1 字节
2. **成员`b`(`uint16_t`)**:
* 要求 2 字节对齐(地址需为 2 的倍数)
* 上一个成员结束于地址 0,需填充 1 字节(地址 1)
* `b`从地址 2 开始,占用地址 2-3(2 字节)
* 已用内存:1 + 1(填充) + 2 = 4 字节
3. **成员`c`(`uint8_t`)**:
* 占 1 字节,地址 4(满足 1 字节对齐)
* 已用内存:4 + 1 = 5 字节
4. **成员`d`(`uint16_t`)**:
* 要求 2 字节对齐(地址需为 2 的倍数)
* 上一个成员结束于地址 4,下一个地址 5 不是 2 的倍数,需填充 1 字节(地址 5)
* `d`从地址 6 开始,占用地址 6-7(2 字节)
* 已用内存:5 + 1(填充) + 2 = 8 字节
5. **成员`e`(`uint32_t`)**:
* 要求 4 字节对齐(地址需为 4 的倍数)
* 上一个成员结束于地址 7,下一个地址 8 正好是 4 的倍数
* `e`从地址 8 开始,占用地址 8-11(4 字节)
* 已用内存:8 + 4 = 12 字节
6. **结构体总大小**:
* 最大成员对齐值为 4(`uint32_t`)
* 12 是 4 的整数倍,无需额外填充
* 最终大小:**12 字节**
### 3.2 输出结果
结构体大小: 12 字节
### 3.3 关键结论
* 总大小不是简单相加(1+2+1+2+4=10 字节),而是因对齐填充了 2 字节(地址 1 和地址 5)
* 成员顺序会影响总大小,合理排列成员(如将相同对齐要求的成员放在一起)可减少填充
// 优化后(大小仍为12字节,此处巧合)
struct MyStruct {
uint16_t b; // 2字节
uint16_t d; // 2字节
uint8_t a; // 1字节
uint8_t c; // 1字节
uint32_t e; // 4字节
};
## 04-示例3
struct MyStruct {
uint16_t b; // 2字节
uint16_t d; // 2字节
uint8_t a; // 1字节
uint8_t c; // 1字节
uint32_t e; // 4字节
uint8_t f;
};
我们来分析这个包含`uint16_t`、`uint8_t`、`uint32_t`等成员的结构体大小,重点依然遵循内存对齐规则 \*\*。
### 4.1 结构体大小计算过程
结构体大小计算过程(地址从 0 开始)
#include
#include
struct MyStruct {
uint16_t b; // 2字节
uint16_t d; // 2字节
uint8_t a; // 1字节
uint8_t c; // 1字节
uint32_t e; // 4字节
uint8_t f; // 1字节
};
int main() {
printf("结构体 MyStruct 大小: %zu 字节\n", sizeof(struct MyStruct));
return 0;
}
#### 分步计算:
1. **成员`b`(`uint16_t`)**:
* 占 2 字节,从地址 0 开始(满足 2 字节对齐)
* 已用内存:2 字节
2. **成员`d`(`uint16_t`)**:
* 占 2 字节,从地址 2 开始(满足 2 字节对齐)
* 已用内存:2 + 2 = 4 字节
3. **成员`a`(`uint8_t`)**:
* 占 1 字节,从地址 4 开始(满足 1 字节对齐)
* 已用内存:4 + 1 = 5 字节
4. **成员`c`(`uint8_t`)**:
* 占 1 字节,从地址 5 开始(满足 1 字节对齐)
* 已用内存:5 + 1 = 6 字节
5. **成员`e`(`uint32_t`)**:
* 要求 4 字节对齐(地址需为 4 的倍数)
* 上一个成员结束于地址 5,下一个可用地址 6 不是 4 的倍数,需填充 2 字节(地址 6-7)
* `e`从地址 8 开始,占用地址 8-11(4 字节)
* 已用内存:6 + 2(填充) + 4 = 12 字节
6. **成员`f`(`uint8_t`)**:
* 占 1 字节,从地址 12 开始(满足 1 字节对齐)
* 已用内存:12 + 1 = 13 字节
7. **结构体总大小**:
* 最大成员对齐值为 4(`uint32_t`)
* 当前已用 13 字节,需要填充 3 字节使总大小为 4 的整数倍(13 + 3 = 16)
* 最终大小:**16 字节**
### 4.2 输出结果:
结构体 MyStruct 大小: 16 字节
### 4.3 内存布局示意图:
| 地址范围 | 内容 | 说明 |
|-------|---------|-----------------|
| 0-1 | `b` | 2 字节,`uint16_t` |
| 2-3 | `d` | 2 字节,`uint16_t` |
| 4 | `a` | 1 字节,`uint8_t` |
| 5 | `c` | 1 字节,`uint8_t` |
| 6-7 | 填充(无数据) | 为`e`对齐到 4 字节 |
| 8-11 | `e` | 4 字节,`uint32_t` |
| 12 | `f` | 1 字节,`uint8_t` |
| 13-15 | 填充(无数据) | 使总大小为 4 的整数倍 |
### 4.4 总结:
* 成员总大小为:2+2+1+1+4+1 = 11 字节
* 因对齐填充了 5 字节(6-7 地址 2 字节,13-15 地址 3 字节)
* 最终结构体大小为 16 字节(满足最大对齐值 4 的整数倍要求)
### 4.5 优化
优化结构体成员的排布可以减少内存对齐带来的填充字节,从而减小结构体总大小。核心原则是:**将对齐要求相同或相近的成员放在一起,并且按对齐值从大到小(或从小到大)排列**。
针对你的结构体,我们可以这样优化:
#include
#include
// 优化前的结构体(大小16字节)
struct MyStructOriginal {
uint16_t b; // 2字节
uint16_t d; // 2字节
uint8_t a; // 1字节
uint8_t c; // 1字节
uint32_t e; // 4字节
uint8_t f; // 1字节
};
// 优化后的结构体(大小12字节)
struct MyStructOptimized {
uint32_t e; // 4字节(最高对齐要求,放最前)
uint16_t b; // 2字节
uint16_t d; // 2字节(与b对齐要求相同,放一起)
uint8_t a; // 1字节
uint8_t c; // 1字节(与a对齐要求相同,放一起)
uint8_t f; // 1字节(与a、c对齐要求相同,放一起)
};
int main() {
printf("优化前大小: %zu 字节\n", sizeof(struct MyStructOriginal));
printf("优化后大小: %zu 字节\n", sizeof(struct MyStructOptimized));
return 0;
}
#### 优化后结构体的内存布局分析(地址从 0 开始):
1. **成员`e`(`uint32_t`)**:
* 占 4 字节,从地址 0 开始(满足 4 字节对齐)
* 已用内存:4 字节
2. **成员`b`(`uint16_t`)**:
* 占 2 字节,从地址 4 开始(满足 2 字节对齐)
* 已用内存:4 + 2 = 6 字节
3. **成员`d`(`uint16_t`)**:
* 占 2 字节,从地址 6 开始(满足 2 字节对齐)
* 已用内存:6 + 2 = 8 字节
4. **成员`a`(`uint8_t`)**:
* 占 1 字节,从地址 8 开始(满足 1 字节对齐)
* 已用内存:8 + 1 = 9 字节
5. **成员`c`(`uint8_t`)**:
* 占 1 字节,从地址 9 开始(满足 1 字节对齐)
* 已用内存:9 + 1 = 10 字节
6. **成员`f`(`uint8_t`)**:
* 占 1 字节,从地址 10 开始(满足 1 字节对齐)
* 已用内存:10 + 1 = 11 字节
7. **结构体总大小**:
* 最大成员对齐值为 4(`uint32_t`)
* 当前已用 11 字节,需填充 1 字节使总大小为 4 的整数倍(11 + 1 = 12)
* 最终大小:**12 字节**(比优化前减少 4 字节)
## 05-优化核心思路:
1. **优先放置高对齐要求的成员** :将`uint32_t e`(4 字节对齐)放在最前面,避免后续成员为了对齐它而产生大量填充。
2. **同类对齐成员集中放置** :将`uint16_t`类型的`b`和`d`放在一起,`uint8_t`类型的`a`、`c`、`f`放在一起,减少跨类型对齐产生的填充。
3. **最小化填充字节**:优化后仅在结构体末尾需要 1 字节填充(而优化前需要 5 字节填充)。
通过合理调整成员顺序,在不改变功能的前提下,有效减少了结构体的内存占用。