Why this matters
You might expect a struct's size to be the sum of its members' sizes. Often it is not: the compiler inserts invisible padding bytes. This matters whenever you allocate memory for structs (always use sizeof, never add up sizes by hand), when you store millions of them (a badly ordered struct can be 50 percent larger), when you write structs to files or send them over a network (the bytes include padding), and when you compare them. Understanding layout also explains a hardware fact that shapes all low-level code: alignment.
Alignment
Processors read memory most efficiently, and on some architectures only, when a value of size N sits at an address that is a multiple of N. A 4-byte int wants an address divisible by 4, an 8-byte double an address divisible by 8. This requirement is the type's alignment. C11 added _Alignof to ask the compiler for it:
printf("alignof: char %zu, short %zu, int %zu, double %zu, pointer %zu\n",
_Alignof(char), _Alignof(short), _Alignof(int), _Alignof(double), _Alignof(char *));
alignof: char 1, short 2, int 4, double 8, pointer 8
Why hardware cares: memory is fetched in aligned chunks (cache lines of 64 bytes, words of 8). An aligned 8-byte value always lies within one chunk. A misaligned one can straddle two, costing two fetches, and some processors (older ARM, many embedded chips) simply fault on misaligned access. On x86-64 misaligned access works but can be slower. C's rule is simple: every object must be correctly aligned for its type, and the compiler arranges that for you.
Padding inside structs
Members are laid out in declaration order; the compiler is not allowed to reorder them. To keep each member aligned, it inserts padding before members as needed, and at the end so the whole struct's size is a multiple of its own alignment (which is the largest alignment of any member).
offsetof(type, member) from <stddef.h> reports where each member starts:
#include <stdio.h>
#include <stddef.h>
struct bad {
char flag;
double value;
char tag;
};
struct good {
double value;
char flag;
char tag;
};
int main(void)
{
printf("struct bad: size %zu, flag @%zu, value @%zu, tag @%zu\n",
sizeof(struct bad), offsetof(struct bad, flag),
offsetof(struct bad, value), offsetof(struct bad, tag));
printf("struct good: size %zu, value @%zu, flag @%zu, tag @%zu\n",
sizeof(struct good), offsetof(struct good, value),
offsetof(struct good, flag), offsetof(struct good, tag));
struct bad arr[3];
printf("sizeof arr = %zu\n", sizeof arr);
return 0;
}
struct bad: size 24, flag @0, value @8, tag @16
struct good: size 16, value @0, flag @8, tag @9
sizeof arr = 72
Drawn out, one byte per cell, P for padding:
struct bad (24 bytes, 14 of them padding)
offset 0 1 2 3 4 5 6 7 8 ...........15 16 17 ......... 23
+---+---+---+---+---+---+---+---+-----------------+---+---------------+
| f | P | P | P | P | P | P | P | value (double) | t | P P P P P P P |
+---+---+---+---+---+---+---+---+-----------------+---+---------------+
flag 7 bytes so value is at a multiple of 8 tag trailing padding
struct good (16 bytes, 6 of them padding)
offset 0 ............. 7 8 9 10 ............ 15
+-------------------+---+---+-------------------+
| value (double) | f | t | P P P P P P |
+-------------------+---+---+-------------------+
Same three members, a third less memory, just by ordering them from largest to smallest alignment.
Why trailing padding?
struct bad ends with 7 bytes of padding after tag, even though nothing follows. Consider an array: arr[1] starts at arr + sizeof(struct bad). If the size were 17, arr[1].value would be at offset 17 + 8 = 25, not a multiple of 8. Padding the size up to 24 makes every element of an array start properly aligned. That is also why sizeof arr is exactly 3 * 24 = 72: arrays never have gaps between elements, so the gap has to be inside the struct.
The ordering rule of thumb
To minimize padding, declare members in decreasing order of alignment: 8-byte members (double, long, pointers, size_t) first, then 4-byte (int, float), then 2-byte (short), then 1-byte (char, char arrays). Group small members together so they share what would otherwise be one padding gap.
Readability still matters. For a struct you create a handful of, keep the logical order that makes the code clearest. Reorder when you store many of them, or when a profiler says memory is the problem.
gcc can show you every padding gap with -Wpadded (not part of -Wall; it is purely informational):
p1.c:6:12: warning: padding struct to align 'value' [-Wpadded]
p1.c:8:1: warning: padding struct size to alignment boundary with 7 bytes [-Wpadded]
Consequences you will meet
- Always allocate with
sizeof.malloc(n * sizeof(struct bad)), or bettermalloc(n * sizeof *p). Adding up member sizes gives 10 forstruct bad, and writing structs into that would overflow the block. - Structs are not a file format. Writing a struct's raw bytes with
fwrite(files module) writes the padding too, and the layout differs between compilers and architectures (and the byte order of each number differs between machines, covered in the bits module). Raw struct dumps are only readable by the same program on the same kind of machine. - Do not compare with
memcmp. Padding bytes have unspecified values. - Packed structs. gcc's
__attribute__((packed))removes padding (struct { char c; int i; }becomes 5 bytes). It is non-standard, makes member access slower or unsafe on some processors, and taking the address of a packed member is a trap. It exists for matching exact hardware or protocol layouts; avoid it otherwise.
Common mistakes
- Hand-computing sizes:
malloc(count * (1 + 8 + 1))forstruct bad. Heap overflow; valgrind reportsInvalid write of size 8. - Assuming the layout of someone else's struct (for example the offset of a member in a library struct). Use
offsetofandsizeof; layouts change between versions and platforms. - Assuming no padding between
chararrays and following members:struct { char c[5]; int i; }is 12 bytes, not 9.
Recap
- Every type has an alignment; objects must sit at addresses that are multiples of it.
- Struct members stay in declaration order; the compiler pads before members to align them and at the end so the size is a multiple of the struct's alignment (making arrays work).
offsetof(type, member)andsizeofshow the real layout;-Wpaddedpoints out each gap.- Order members from largest to smallest alignment to minimize padding.
- Never hand-compute struct sizes,
memcmpstructs, or treat raw struct bytes as a portable format.
Going further: predict, then check with a small program, the size of struct { char c; long l; char d; int i; } (it is 24; can you get it to 16?). Then add -Wpadded to see gcc explain each gap.