C/C++结构体内存的问题
1.结构体内存布局的基本规则
1.1.成员顺序
结构体的成员在内存中按照声明的顺序依次存放。第一个成员的地址与结构体的地址相同。
struct Example { char a; // 1字节 int b; // 4字节 double c; // 8字节 short d; // 2字节 };1.2 对齐要求和结构体大小的计算
结构体的大小不是简单地将所有成员的大小相加,而是要考虑对齐和填充。每个基本类型(如int、double、指针等)都有对齐要求。对齐要求通常是该类型的大小(以字节为单位)或者是编译器特定的值。例如,在大多数系统上,int的对齐要求是4字节,double的对齐要求是8字节。
内存对齐:
内存对齐是通过在数据间填充少量字节,使数据的起始地址满足 CPU 字长(32 位为 4 字节、64 位为 8 字节)或数据自身大小的整数倍的内存布局规则,其核心意义在于:以牺牲少量内存形成内部碎片为代价,既大幅提升 CPU 按访问单元批量读取内存的效率(避免非对齐时多次读取、拼接数据的额外开销),又能兼容不支持非对齐访问的硬件(如早期 ARM 处理器)以防止程序崩溃,同时还能避免数据跨 CPU 访问单元存储引发的截断、乱码等问题,保障数据存储的完整性与稳定性。
默认对齐规则
(无 #pragma pack,编译器默认行为)
| 数据类型分类 | 对齐值计算规则 | 64 位系统示例(对齐值 / 大小) | 32 位系统示例(对齐值 / 大小) |
|---|---|---|---|
| 基本数据类型 | 对齐值 = 该类型的「字节大小」(大小就是 sizeof 的结果) | char:1/1short:2/2int:4/4double:8/8 | 同 64 位(基本类型大小不变) |
| 指针类型 | 对齐值 = CPU 字长(64 位 = 8,32 位 = 4),与指向的类型无关 | int*:8/8char*:8/8double*:8/8 | int*:4/4char*:4/4 |
| 数组类型 | 对齐值 = 「数组元素的对齐值」(数组整体大小不影响对齐值,只影响占用空间) | char arr[5]:1/5int arr[3]:4/12double arr[2]:8/16 | 同 64 位(元素对齐值不变) |
| 结构体(struct) | 对齐值 = 其「所有成员对齐值的最大值」(和成员大小无关,只看成员的对齐值) | 示例: struct S {char c; double d;}对齐值 = max (1,8)=8 | 同 64 位(成员对齐值不变) |
| 联合体(union) | 对齐值 = 其「所有成员对齐值的最大值」(和成员大小无关,只看成员的对齐值) | 示例: union U {int a; char b[5];}对齐值 = max (4,1)=4 | 同 64 位(成员对齐值不变) |
| 嵌套结构体 | 对齐值 = 该嵌套结构体「自身的对齐值」(而非嵌套结构体的大小) | 示例: struct Sub {char a; double b;} // Sub对齐值 8 struct S {char c; Sub s;} // S 的对齐值 = max (1,8)=8 | 同 64 位 |
计算规则:
规则1:如果你有一个成员_e(大小为x字节对齐值假设也为x字节)需要加入到相对结构体起始地址0偏移y byte的位置那么需要校验y是否为对齐值x的整数倍,若不为整数倍则需加最少的n字节,使得(y+n)%x = 0;
规则2:结构体总大小必须是最大对齐值的整数倍, 而非成员大小的最大值.
规则3:若类有虚函数或者虚继承,则编译器会隐式地加一个虚函数指针的大小在开头;若继承多个不同类型的父类,则会继承到多个虚函数表指针. 这些虚表指针需要计入类的大小。
基本示例:
struct Example1 { char a; // 1字节 int b; // 4字节 double c; // 8字节 }; // 由于内存对齐,可能是16字节 struct Example2 { char a; // 偏移0 double b; // 偏移8(不能放在1) char c; // 偏移16 }; // 总大小24字节(不是17,因为24是8的倍数) // 内存布局: // [a][填充7字节][b(8字节)][c][填充7字节] class MyClass { private: int a; // 4字节 double b; // 8字节 static int c; // 不计入对象大小 public: void func() {} // 函数不计入对象大小 };2.一些案例
关于嵌套结构体:
struct Inner { int x; // 4字节 char y; // 1字节 // 填充3字节 }; // 大小: 8字节 struct Outer { short a; // 2字节 // 填充2字节 Inner inner; // 8字节 (从4字节边界开始) char b; // 1字节 // 填充7字节 }; // 大小: 24字节3.一个特殊用法!!!
对于任何一个结构体,可以获取到任何一个元素相较于结构体起点的偏移量:
&((struct A*)0->_element)
结构体指针指向的就是结构体的起点, 同时此起点有和起始元素的地址相同;
只要我们拥有结构体某一个元素的指针,那么就可以用这个指针加上偏移量得到整个结构体的指针
可以用预处理指令定义一个宏offsetof:
#define offsetof(TYPE, MEMBER)((size_t)&((TYPE *)0)->MEMBER)4.大小端问题
结构体本身并不决定字节序(大端或小端)也不参与字节序, 字节序是由底层的硬件架构决定的。结构体的内存布局中, 成员按照声明顺序排列(考虑对齐), 但每个成员内部的字节顺序则取决于系统的字节序。
大端存储(Big-endian):高位字节存储在低地址, 低位字节存储在高地址
小端存储(Little-endian):低位字节存储在低地址, 高位字节存储在高地址
/* 结构体内部遵循硬件平台的字节序 x86/x64架构:小端序 ARM(可配置):通常小端序 PowerPC、SPARC:大端序 网络字节序:大端序 */