考虑以下情况:
struct mystruct_A
{
char a;
int b;
char c;
} x;
struct mystruct_B
{
int b;
char a;
} y;
这些结构的大小分别为12和8。
这些结构是填充还是紧凑的?
填充或者紧凑是在什么时候发生的?
填充 对齐 结构体成员到“自然”地址边界 - 比如,在32位平台上,int
成员将具有 mod(4) == 0
的偏移量。填充默认开启。它会在您的第一个结构体中插入以下“间隙”:
struct mystruct_A {
char a;
char gap_0[3]; /* inserted by compiler: for alignment of b */
int b;
char c;
char gap_1[3]; /* -"-: for alignment of the whole struct in an array */
} x;
__attribute__((__packed__))
,因此以下内容:struct __attribute__((__packed__)) mystruct_A {
char a;
int b;
char c;
};
在32位结构上,会生成大小为6
的结构。
需要注意的是,在允许不对齐内存访问的架构(如x86和amd64)上,不对齐内存访问速度较慢,并且在像SPARC这样的严格对齐架构上明确禁止。
(以上答案已经很清楚地解释了原因,但似乎没有完全清楚填充的大小,因此,我将根据我从The Lost Art of Structure Packing了解到的内容添加一个答案,它已发展到不仅限于C
,还适用于Go
,Rust
。)
规则:
int
应该从可被 4 整除的地址开始,而 short
应该从可被 2 整除的地址开始。char
和 char[]
是特殊的,可以是任何内存地址,因此它们不需要在它们之前进行填充。struct
,除了每个单独成员的对齐需求外,整个结构体本身的大小将通过在结尾填充以使其大小对最严格对齐要求的成员任何一个整除,int
,则应该被 4 整除,如果是 short
,则应该被 2 整除。成员的顺序:
stu_c
和 stu_d
具有相同的成员,但顺序不同,导致 2 个结构的大小不同。空隙:
test_struct_address()
中,变量 x
位于相邻结构体 g
和 h
之间。x
还是没有声明,h
的地址都不会改变,x
只是重新使用了 g
浪费的空间。y
的情况类似。(针对 64 位系统)
memory_align.c:
/**
* Memory align & padding - for struct.
* compile: gcc memory_align.c
* execute: ./a.out
*/
#include <stdio.h>
// size is 8, 4 + 1, then round to multiple of 4 (int's size),
struct stu_a {
int i;
char c;
};
// size is 16, 8 + 1, then round to multiple of 8 (long's size),
struct stu_b {
long l;
char c;
};
// size is 24, l need padding by 4 before it, then round to multiple of 8 (long's size),
struct stu_c {
int i;
long l;
char c;
};
// size is 16, 8 + 4 + 1, then round to multiple of 8 (long's size),
struct stu_d {
long l;
int i;
char c;
};
// size is 16, 8 + 4 + 1, then round to multiple of 8 (double's size),
struct stu_e {
double d;
int i;
char c;
};
// size is 24, d need align to 8, then round to multiple of 8 (double's size),
struct stu_f {
int i;
double d;
char c;
};
// size is 4,
struct stu_g {
int i;
};
// size is 8,
struct stu_h {
long l;
};
// test - padding within a single struct,
int test_struct_padding() {
printf("%s: %ld\n", "stu_a", sizeof(struct stu_a));
printf("%s: %ld\n", "stu_b", sizeof(struct stu_b));
printf("%s: %ld\n", "stu_c", sizeof(struct stu_c));
printf("%s: %ld\n", "stu_d", sizeof(struct stu_d));
printf("%s: %ld\n", "stu_e", sizeof(struct stu_e));
printf("%s: %ld\n", "stu_f", sizeof(struct stu_f));
printf("%s: %ld\n", "stu_g", sizeof(struct stu_g));
printf("%s: %ld\n", "stu_h", sizeof(struct stu_h));
return 0;
}
// test - address of struct,
int test_struct_address() {
printf("%s: %ld\n", "stu_g", sizeof(struct stu_g));
printf("%s: %ld\n", "stu_h", sizeof(struct stu_h));
printf("%s: %ld\n", "stu_f", sizeof(struct stu_f));
struct stu_g g;
struct stu_h h;
struct stu_f f1;
struct stu_f f2;
int x = 1;
long y = 1;
printf("address of %s: %p\n", "g", &g);
printf("address of %s: %p\n", "h", &h);
printf("address of %s: %p\n", "f1", &f1);
printf("address of %s: %p\n", "f2", &f2);
printf("address of %s: %p\n", "x", &x);
printf("address of %s: %p\n", "y", &y);
// g is only 4 bytes itself, but distance to next struct is 16 bytes(on 64 bit system) or 8 bytes(on 32 bit system),
printf("space between %s and %s: %ld\n", "g", "h", (long)(&h) - (long)(&g));
// h is only 8 bytes itself, but distance to next struct is 16 bytes(on 64 bit system) or 8 bytes(on 32 bit system),
printf("space between %s and %s: %ld\n", "h", "f1", (long)(&f1) - (long)(&h));
// f1 is only 24 bytes itself, but distance to next struct is 32 bytes(on 64 bit system) or 24 bytes(on 32 bit system),
printf("space between %s and %s: %ld\n", "f1", "f2", (long)(&f2) - (long)(&f1));
// x is not a struct, and it reuse those empty space between struts, which exists due to padding, e.g between g & h,
printf("space between %s and %s: %ld\n", "x", "f2", (long)(&x) - (long)(&f2));
printf("space between %s and %s: %ld\n", "g", "x", (long)(&x) - (long)(&g));
// y is not a struct, and it reuse those empty space between struts, which exists due to padding, e.g between h & f1,
printf("space between %s and %s: %ld\n", "x", "y", (long)(&y) - (long)(&x));
printf("space between %s and %s: %ld\n", "h", "y", (long)(&y) - (long)(&h));
return 0;
}
int main(int argc, char * argv[]) {
test_struct_padding();
// test_struct_address();
return 0;
}
执行结果 - test_struct_padding()
:
stu_a: 8
stu_b: 16
stu_c: 24
stu_d: 16
stu_e: 16
stu_f: 24
stu_g: 4
stu_h: 8
执行结果 - test_struct_address()
:
stu_g: 4
stu_h: 8
stu_f: 24
address of g: 0x7fffd63a95d0 // struct variable - address dividable by 16,
address of h: 0x7fffd63a95e0 // struct variable - address dividable by 16,
address of f1: 0x7fffd63a95f0 // struct variable - address dividable by 16,
address of f2: 0x7fffd63a9610 // struct variable - address dividable by 16,
address of x: 0x7fffd63a95dc // non-struct variable - resides within the empty space between struct variable g & h.
address of y: 0x7fffd63a95e8 // non-struct variable - resides within the empty space between struct variable h & f1.
space between g and h: 16
space between h and f1: 16
space between f1 and f2: 32
space between x and f2: -52
space between g and x: 12
space between x and y: 12
space between h and y: 8
因此,每个变量的起始地址为 g:d0 x:dc h:e0 y:e8。
<C结构体打包的失传艺术>
很好地解释了规则,虽然比这个回答稍微长一些。该书可以在网上免费获取:http://www.catb.org/esr/structure-packing/ - Eric我知道这个问题已经很老了,这里的大多数答案都很好地解释了填充,但在尝试理解它时,我发现有一个“视觉”图像可以帮助。
处理器以固定大小(字)的“块状”方式读取内存。假设处理器字长为8个字节。它将查看内存作为大型的8个字节的构建块行。每次需要从内存获取一些信息时,它将到达其中一个块并获取它。
如上图所示,Char(1个字节长)位于哪里并不重要,因为它将位于这些块中的一个内部,只需要CPU处理1个字。
当我们处理比一个字节更大的数据时,例如4字节int或8字节double,它们在内存中的对齐方式会影响CPU必须处理多少个字。如果4字节块以它们总是适合块内(内存地址是4的倍数)的方式进行对齐,则只需要处理一个字。否则,4字节的块可能部分在一个块上,部分在另一个块上,需要处理2个字才能读取这些数据。
对于8字节的double,情况也是一样,只不过现在它必须位于8的倍数的内存地址中,以确保它总是在块内。
这适用于8字节字处理器,但概念适用于其他字大小。
填充通过填充这些数据之间的间隙来确保它们与这些块对齐,从而改善读取内存的性能。
然而,正如其他答案中所述,有时空间比性能更重要。也许您正在计算机上处理大量数据,而该计算机没有太多的RAM(可以使用swap space,但速度要慢得多)。您可以将程序中的变量排列到最少填充的方式(在其他答案中进行了很好的说明),但如果这还不够,可以显式禁用填充,这就是packing的作用。
memcpy
等将其复制到结构体中。 - user3342816结构体紧凑排列可以抑制结构体填充,填充用于对齐最重要的情况,紧凑排列用于空间最为重要的情况。
一些编译器提供#pragma
来抑制填充或使其紧凑到n字节。有些编译器提供关键字来实现这一点。通常用于修改结构体填充的#pragma
的格式如下(取决于编译器):
#pragma pack(n)
例如,ARM提供了__packed
关键字来抑制结构填充。请参阅您的编译器手册以了解更多信息。
因此,紧凑结构是没有填充的结构。
通常,紧凑结构将被用于:
节省空间
使用某些协议在网络上传输数据结构的格式(当然这不是一个好的实践,因为您需要处理Endianness)
填充和对齐是同一事物的两个方面:
在默认对齐方式为4的情况下,mystruct_A
中的每个成员都按照4字节的倍数进行对齐。由于char
的大小为1,所以a
和c
的填充为4-1=3字节,而对于已经是4字节的int b
则不需要填充。mystruct_B
同样也是如此。
int main(void) {
// We assume the `c` is stored as first byte of machine word
// as a convenience! If the `c` was stored as a last byte of previous
// word, there is no need to pad bytes before variable `i`
// because `i` is automatically aligned in a new word.
char c; // starts from any addresses divisible by 1(any addresses).
char pad[3]; // not-used memory for `i` to start from its address.
int32_t i; // starts from any addresses divisible by 4.
这类似于结构体,但有一些区别。首先,我们可以说有两种填充方式— a) 为了使每个成员从正确的地址开始,需要在成员之间插入一些字节。b) 为了使下一个结构体实例从其正确的地址开始,需要在每个结构体后附加一些字节:
// Example for rule 1 below.
struct st {
char c; // starts from any addresses divisible by 4, not 1.
char pad[3]; // not-used memory for `i` to start from its address.
int32_t i; // starts from any addresses divisible by 4.
};
// Example for rule 2 below.
struct st {
int32_t i; // starts from any addresses divisible by 4.
char c; // starts from any addresses.
char pad[3]; // not-used memory for next `st`(or anything that has same
// alignment requirement) to start from its own address.
};
int32_t
的对齐)决定。这与普通变量不同。普通变量可以从其对齐的任何地址开始,但对于结构体的第一个成员则不是这种情况。如您所知,结构体的地址与其第一个成员的地址相同。struct st arr[2];
。为使arr [1]
(arr [1]
的第一个成员)从可被4整除的地址开始,我们应在每个结构体的末尾附加3个字节。这是我从The Lost Art of Structure Packing了解到的。
注意:您可以通过_Alignof
运算符确定数据类型的对齐要求。此外,您可以通过offsetof
宏获取结构体中成员的偏移量。
填充规则:
为什么需要第二条规则:
考虑以下结构体:
如果我们要创建一个该结构体的数组(2个结构体),则不需要在末尾添加填充:
因此,结构体大小=8个字节
现在假设我们要创建另一个结构体:
如果我们要创建一个该结构体的数组,有2种可能需要在末尾添加填充:
A.如果我们在末尾添加3个字节并对齐到int:
B.如果我们在末尾添加7个字节并对齐到Long:
第二个数组的起始地址是8的倍数(即24)。 结构体大小=24个字节
因此,通过将该结构体的下一个数组的起始地址对齐为最大成员的倍数(即如果我们要创建该结构体的数组,则第二个数组的第一个地址必须从该结构体的最大成员的倍数开始。这里是24(3 * 8)),我们可以计算出在末尾需要多少字节的填充。
// C11 or better ...
#include <assert.h>
struct strA { char a; int b; char c; } x;
struct strB { int b; char a; } y;
static_assert(sizeof(struct strA) == sizeof(char)*2 + sizeof(int), "No padding allowed");
static_assert(sizeof(struct strB) == sizeof(char) + sizeof(int), "No padding allowed");
如果这些结构中有任何填充,类似这样的内容将拒绝编译。
结构体打包只有在您明确告诉编译器打包结构体时才会进行。所看到的是填充。您的32位系统将每个字段填充到字对齐。如果您已经告诉编译器打包结构体,它们分别为6和5个字节。不要这样做。它不具备可移植性,并且会使编译器生成更慢(有时甚至是有缺陷的)代码。
毫无疑问! 想要掌握这个主题的人必须完成以下步骤:
- 阅读Eric S. Raymond所写的《结构体打包的失落艺术》
- 浏览Eric的代码示例
- 最后但同样重要的是,不要忘记这个关于填充的规则:一个结构体对齐到最大类型的对齐要求。
padding
会使事物变得更大。packing
则会使事物变得更小。两者完全不同。 - smwikipedia