【C语言】结构体字节对齐
【C语言】结构体字节对齐
文章目录
- 【C语言】结构体字节对齐
- 1.简介
- 2.举例
- 坑1:
- 坑2:
- 3.为什么存在内存对齐?
- 4.包含指针的结构体
- 5.包含数组的结构体
- 示例 1:结构体中的整型数组
- 示例 2:结构体中的字符数组
- 示例 3:结构体中的混合类型数组
- 注意事项
1.简介
在C语言中,结构体(struct)的字节对齐(也称为内存对齐或数据对齐)是一种内存布局的优化手段,它涉及到结构体成员在内存中的排列方式。字节对齐的主要目的是提高内存访问的效率,因为大多数现代处理器在访问按照其字长(如32位或64位)对齐的内存地址时速度更快。
以下是关于结构体字节对齐的一些关键点:
-
自然对齐(成员对齐):每个成员变量的起始地址都是其大小的整数倍。例如,如果一个结构体包含一个
int
(假设int
是4字节)和一个char
(1字节),那么int
成员的起始地址应该是4的倍数,而char
成员的起始地址可以是任意地址。 -
结构体对齐:整个结构体的大小也是其最大成员大小的整数倍。这意味着结构体的总大小可能会因为填充字节而增加。
-
填充字节:编译器会在结构体的成员之间或结构体的末尾添加填充字节,以确保每个成员都满足其自然对齐要求,并且结构体本身也满足对齐要求。
-
编译器控制:编译器默认会进行字节对齐,但程序员也可以通过特定的编译器指令或属性来控制对齐方式,例如在GCC中使用
__attribute__((packed))
来取消对齐。 -
平台依赖性:字节对齐的具体规则可能会因编译器和目标平台的不同而有所差异。
-
性能影响:不正确的对齐可能会导致性能下降,甚至在某些架构上导致程序崩溃。
-
跨平台兼容性:在设计需要在不同平台间移植的结构体时,需要考虑到字节对齐的差异。
-
结构体成员的顺序:通常,将大的成员放在结构体的前面可以减少填充字节,从而减少结构体的总大小。
下面是一个简单的结构体字节对齐的例子:
struct Example {
char a; // 1 byte
int b; // 4 bytes on most platforms
char c; // 1 byte
};
占字节大小:为12个字节
a000 bbbb c000
//如果交换位置呢?
struct Example {
int b; // 4 bytes on most platforms
char a; // 1 byte
char c; // 1 byte
};
占字节大小:为8个字节
bbbb ac00
由此看出,结构体成员的顺序及其重要
在这个例子中,b
成员需要4字节对齐,所以a
和c
之间可能会有3字节的填充,以确保b
的地址是4的倍数。整个结构体的大小也会是其最大成员大小(在这个例子中是int
)的整数倍。
了解和合理利用字节对齐对于优化程序性能和确保跨平台兼容性是非常重要的。
总结出来就几句话:
- 整个结构体的大小也是其最大成员大小的整数倍
- 每个成员变量的起始地址都是其最大成员大小的整数倍
- 编译器会在结构体的成员之间或结构体的末尾添加填充字节,以确保每个成员都满足其自然对齐要求,并且结构体本身也满足对齐要求。
2.举例
typedef struct A{
char a;
char b;
char c;
char d;
}structA;
typedef struct B{
char a;
short b;
char c;
char d;
}structB;
typedef struct C{
char a;
int b;
char c;
char d;
}structC;
typedef struct D{
char a;
double b;
char c;
char d;
}structD;
2.1先看结构体A:
假如结构体起始地址是0x0000,
成员a的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0000是1的整数倍,故a存放起始地址是0x0000,占一个字节;
成员b的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0001是1的整数倍,故b存放起始地址是0x0001,占一个字节;
成员c的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0002是1的整数倍,故c存放起始地址是0x0002,占一个字节;
成员d的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0003是1的整数倍,故d存放起始地址是0x0003,占一个字节;
此时结构体A的有效对齐值是其最大数据成员的自身对齐值,它的成员都是char类型,故结构体A的有效对齐值是1.
结构体A的存储结构如下,其中Y是根据规则1补齐的字节,x是规则2补齐的字节。
0x0000 | 0x00001 | 0x0002 | 0x0003 |
---|---|---|---|
a | b | c | d |
根据以上规则可以知道其他结构体的存储结构:
2.结构体B占6个字节
a0 bb cd
3.结构体C占12个字节
a000 bbbb cd00
成员a的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0000是1的整数倍,故a存放起始地址是0x0000,占一个字节;
成员b的自身对齐值4,指定对齐值4,所以有效对齐值是4,地址0x0004是4的整数倍,故b存放起始地址是0x0004,占四个字节;
成员c的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0008是1的整数倍,故c存放起始地址是0x0008,占一个字节;
成员d的自身对齐值1,指定对齐值4,所以有效对齐值是1,地址0x0009是1的整数倍,故d存放起始地址是0x0009,占一个字节;
结构体C的成员占据10个字节,而结构体C的有效对齐值是其成员b的自身对齐值4,10不是4的倍数,故还需补齐两个字节,此时结构体C占据12个字节,是4的倍数
4.结构体C占16个字节
坑1:
typedef struct D{
char a;
double b;
char c;
char d;
}structD;
第四个正常应该是24个字节,但是有坑
a000000 bbbbbbbb cd000000
它应该是这样排列的:
a000 bbbbbbbb cd00 ,占16个字节
坑2:
struct node{
int a; //4
double d; //8
char c; //1
};
所占内存大小, 4 + 8 + 1 = 13,因为double类型是8个字节,而前面只有4个字节,并且成员变量最大内存为8,所以应该这样计算 8 + 8 + 8 = 24。
3.为什么存在内存对齐?
总结:提高寻址效率
**1、平台原因:**不是所有的硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常;
**2、性能原因:**数据结构(尤其是栈)应尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存访问仅需要一次访问;32位的系统,有32根数据线,32根数据线,所以地址4字节,32位,读取数据也是一次性读取32位,4字节。64位系统就是地址是八个字节,一次读取64位。
总体来说:
- 结构体的内存对齐是拿空间来换取时间的做法。
- 那在设计的时候,我们既要满足对齐,又要节省空间,
- 如何做到:让占用空间小的成员尽量集中在一起。
4.包含指针的结构体
- 32位系统:在32位系统中,指针通常占用4个字节(32位)。
- 64位系统:在64位系统中,指针通常占用8个字节(64位)。
- ARM架构:在32位的ARM架构中,指针也是4个字节。在64位的ARM架构(如ARMv8-A)中,指针是8个字节。
- x86架构:在32位的x86架构中,指针是4个字节。在64位的x86架构(通常称为x64或AMD64)中,指针是8个字节。
struct Test
{
char a; //1
char *b; //8
double c; //8
} test;
占字节大小:
a0000000 bbbbbbb cccccccc 3*8=24
struct Test
{
char a; //1
int *b; //8
double c; //8
} test;
占字节大小:
a0000000 bbbbbbbb cccccccc 3*8=24
struct Test
{
char a; //1
double *b; //8
double c; //8
} test;
占字节大小:
a0000000 bbbbbbbb cccccccc 3*8=24
struct Test
{
char a; //1
double *b; //8
double c; //8
} test;
占字节大小:
a0000000 bbbbbbbb cccccccc 3*8=24
结构体指针的大小与它所指向的结构体的大小无关。
结构体指针本质还是一个指针
struct Test
{
char a; //1
struct Test *b; //8
double c; //8
} test;
占字节大小:
a0000000 bbbbbbbb cccccccc 3*8=24
5.包含数组的结构体
数组中的元素地址是连续的,所以一个数组所占空间大小,为数组类型 * 元素个数。
知道了数组所占空间大小后,再来说说如何计算结构体中包含数组的情况,在之前计算的时候,我说过相加的时候必须保证前面的成员变量的内存所占内存必须是下一个成员变量所占内存的整倍数,但是如果下一变量为数组,则没有这个要求。
需要注意的是,数组所占用的最大大小不是对齐值,要看数据类型
当结构体中包含数组时,字节对齐规则仍然适用,但是数组的对齐方式可能会受到数组元素类型大小的影响。以下是一些关于结构体中包含数组时字节对齐的示例和详细解释:
示例 1:结构体中的整型数组
struct Example {
int a; // 4 bytes on most platforms
int arr[3]; // 3 * 4 bytes = 12 bytes
char b; // 1 byte, will have 3 bytes of padding to align the next member
};
int a
占用4字节。int arr[3]
占用12字节。char b
占用1字节,后面跟着3字节的填充,以确保整个结构体4字节对齐。
总大小:4 (a) + 12 (arr) + 1 (b) + 3 (填充) = 20字节。
示例 2:结构体中的字符数组
struct Example {
char a[4]; // 4 bytes
int b; // 4 bytes, will have 4 bytes of padding if necessary
double c; // 8 bytes on most platforms, will have 4 bytes of padding if necessary
};
在这个例子中,char a[4]
占用4字节。如果int
类型需要4字节对齐,那么b
的地址必须是4的倍数。如果a[4]
后面没有足够的空间来满足b
的对齐要求,编译器会在a[4]
和b
之间添加填充字节。double c
通常需要8字节对齐,如果b
后面没有足够的空间来满足c
的对齐要求,编译器会在b
和c
之间添加填充字节。
char a[4]
占用4字节。int b
占用4字节。由于double c
需要8字节对齐,b
前面可能需要4字节的填充。double c
占用8字节。
总大小:4 (a) + 4 (b)+ 8 © = 16字节。(也可能是24如果有填充字节的话)
示例 3:结构体中的混合类型数组
struct Example {
char a; // 1 byte
int b[2]; // 2 * 4 bytes = 8 bytes
double c; // 8 bytes, will have 4 bytes of padding if necessary
};
char a
占用1字节。int b[2]
占用8字节。double c
占用8字节。由于double
类型需要8字节对齐
总大小:1 (a) + 7 (填充,以满足8字节对齐) + 8 (b) + 8 © = 24字节。
注意事项
-
数组对齐:数组的对齐通常基于数组元素的类型。例如,如果数组元素是
int
类型,那么数组的起始地址应该是4的倍数。 -
结构体对齐:结构体的总大小也必须满足对齐要求。这意味着结构体的大小通常是其最大成员大小的整数倍,或者是一个编译器和硬件平台定义的对齐边界。
-
填充字节:编译器会在成员变量之间以及结构体的末尾添加填充字节,以确保所有成员变量都满足对齐要求。
-
性能影响:不正确的对齐可能会导致处理器访问内存时性能下降,甚至在某些架构上导致程序崩溃。
-
内存访问模式:某些处理器对连续内存访问有优化,这可能会影响结构体成员的布局,以提高缓存效率。
在设计结构体时,考虑成员变量的顺序和大小可以帮助减少填充字节,从而节省内存空间。使用编译器特定的属性或指令可以进一步控制对齐方式,以满足特定的性能或内存使用要求。
struct Test
{
int a;
char b[21];
int d;
double c;
} test;
应该为 4 + 24 + 4 + 8 = 40。
struct Test
{
int a;
char b[19];
int d;//这仨凑8的倍数
double c;
} test;
应该为 4 + 20 + 8 + 8 = 40。
struct Test
{
char a; //1
char b[19]; //19
int d; //4
double c; //8
} test;
应该为 1 + 19 + 4 + 8 = 32
struct Test
{
char a; //1
char b[17]; //17
int d; //4 这仨拼一起
double c; //8
} test;
应该为 1 + 19 + 4 + 8 = 32。
struct Test
{
char a; //1
char b[15]; //15
int d; //4
double c; //8
} test;
应该为 1 + 15 + 8 + 8 = 32。