考虑:
struct mystruct_A
{
char a;
int b;
char c;
} x;
struct mystruct_B
{
int b;
char a;
} y;
结构尺寸分别为12和8。
这些结构是填充的还是包装的?
什么时候进行填充或包装?
考虑:
struct mystruct_A
{
char a;
int b;
char c;
} x;
struct mystruct_B
{
int b;
char a;
} y;
结构尺寸分别为12和8。
这些结构是填充的还是包装的?
什么时候进行填充或包装?
当前回答
变量存储在可以被其对齐方式(通常是大小)整除的任何地址上。所以,填充/填充不仅仅是为了结构。实际上,所有数据都有自己的对齐要求:
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.
这类似于struct,但有一些区别。首先,我们可以说有两种填充——a)为了正确地从每个成员的地址开始,在成员之间插入一些字节。b)为了正确地从struct的地址启动下一个struct实例,将一些字节追加到每个struct:
// 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.
};
The struct's first member always starts from any addresses divisible by struct's own alignment requirement which is determined by largest member's alignment requirement(here 4, alignment of int32_t). This is different with normal variables. The normal variables can start any addresses divisible by its alignment, but it is not the case for struct's first member. As you know, the address of a struct is the same as the address of its first member. There can be additional padded trailing bytes inside a struct, making next struct(or next element in an array of structs) starting from its own address. Think of struct st arr[2];. To make arr[1](arr[1]'s first member) starting from an address divisible by 4, we should append 3 bytes at the end of each struct.
这是我从《丢失的结构包装艺术》中学到的。
注意:可以通过_Alignof操作符来研究数据类型的对齐要求。同样,你也可以通过offsetof宏来获取结构中成员的偏移量。
其他回答
(上面的答案解释的很清楚,但是对于padding的大小似乎不是很清楚,所以,我将根据我从The Lost Art of Structure Packing学到的补充一个答案,它已经发展到不局限于C,也适用于Go, Rust。)
内存对齐(用于struct)
规则:
Before each individual member, there will be padding so that to make it start at an address that is divisible by its alignment requirement. E.g., on many systems, an int should start at an address divisible by 4 and a short by 2. char and char[] are special, could be any memory address, so they don't need padding before them. For struct, other than the alignment need for each individual member, the size of whole struct itself will be aligned to a size divisible by strictest alignment requirement of any of its members, by padding at end. E.g., on many systems, if struct's largest member is int then by divisible by 4, if short then by 2.
会员顺序:
成员的顺序可能会影响结构的实际大小,所以要记住这一点。 例如,下面示例中的stu_c和stu_d具有相同的成员,但顺序不同,并导致两个结构体的大小不同。
内存中的地址(用于struct)
空的空间:
两个结构体之间的空白空间可以被适合的非结构变量使用。 例如,在下面的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
我知道这个问题很老了,这里的大多数答案都很好地解释了填充,但当我自己试图理解它时,我发现对正在发生的事情有一个“视觉”形象是有帮助的。
处理器以一定大小(字)的“块”读取内存。假设处理器字有8字节长。它将把内存看作一个8字节的大行构建块。每当它需要从内存中获取一些信息时,它就会到达其中一个块并获取它。
如上图所示,一个Char(1字节长)在哪里并不重要,因为它将在其中一个块中,只需要CPU处理1个字。
When we deal with data larger than one byte, like a 4 byte int or a 8 byte double, the way they are aligned in the memory makes a difference on how many words will have to be processed by the CPU. If 4-byte chunks are aligned in a way they always fit the inside of a block (memory address being a multiple of 4) only one word will have to be processed. Otherwise a chunk of 4-bytes could have part of itself on one block and part on another, requiring the processor to process 2 words to read this data.
这同样适用于8字节的double,只不过现在它必须在8的倍数内存地址中,以确保它始终在块中。
这里考虑的是8字节的字处理器,但这个概念也适用于其他大小的字。
填充通过填充这些数据之间的间隙来确保它们与这些块对齐,从而提高读取内存时的性能。
然而,正如其他人回答的那样,有时空间比性能本身更重要。也许您正在一台没有太多RAM的计算机上处理大量数据(可以使用交换空间,但速度要慢得多)。您可以在程序中排列变量,直到完成最少的填充(这在其他一些回答中得到了很好的例子),但如果这还不够,您可以显式地禁用填充,这就是打包。
这些结构是填充的还是包装的?
它们填充。
最初想到的唯一可能是,如果char和int的大小相同,那么char/int/char结构的最小大小将不允许填充,int/char结构也是如此。
然而,这将要求sizeof(int)和sizeof(char)都为4(以获得12和8的大小)。由于sizeof(char)始终为1的标准保证了整个理论的崩溃。
如果char和int的宽度相同,那么大小将是1和1,而不是4和4。因此,为了得到12的大小,在最终字段之后必须有填充。
什么时候进行填充或包装?
只要编译器实现需要。编译器可以在字段之间和最后一个字段之后(但不能在第一个字段之前)插入填充。
这样做通常是为了性能,因为某些类型在特定边界上对齐时性能更好。甚至有一些架构会在你试图访问未对齐的数据时拒绝运行(即崩溃)(是的,我在看你,ARM)。
您通常可以使用特定于实现的特性(如#pragma pack)来控制打包/填充(这实际上是同一领域的两个极端)。即使您不能在特定的实现中这样做,您也可以在编译时检查代码以确保它满足您的需求(使用标准C特性,而不是特定于实现的东西)。
例如:
// 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");
如果这些结构中有任何填充,类似这样的东西将拒绝编译。
这件事没有但是!想要掌握这门学科必须做到以下几点:
细读埃里克·s·雷蒙德所著的《丢失的结构包装艺术》 看一下Eric的代码示例 最后但并非最不重要的是,不要忘记下面关于填充的规则,即结构体的对齐方式与最大类型的对齐方式一致 要求。
只有当你告诉编译器显式地对结构进行打包时,才会进行结构打包。你看到的是填充。您的32位系统正在填充每个字段以字对齐。如果您告诉编译器打包结构,它们将分别为6和5字节。但是不要这样做。它不可移植,使编译器生成的代码更慢(有时甚至有bug)。