C / STRUCTS, UNIONS AND ENUMS
Padding, alignment and ordering members to save space
Predict the exact byte layout of a C struct, explain the padding with offsetof and _Alignof, and reorder members to cut its size without dropping fields.
What you will learn
- Compute a struct's layout by hand from each member's size and alignment
- Read real offsets with offsetof and real alignment with _Alignof
- Reorder members from widest alignment to narrowest to remove interior holes
- Explain why sizeof(struct) can exceed the sum of its members' sizes
Understanding Padding, alignment and ordering members to save space
Every scalar type in C carries an alignment requirement as well as a size: on the usual 64-bit ABIs an int wants an address that is a multiple of 4 and a double an address that is a multiple of 8, because that is how the CPU's load and store paths are wired. When the compiler lays out a struct it must honour those requirements member by member, and it is not allowed to help itself by shuffling members around, since the standard says member addresses increase in declaration order. Its only remaining tool is to insert unnamed padding bytes, and that is where the space goes.
The mental model is a byte cursor. Start at 0, and for each member in declaration order round the cursor up to the next multiple of that member's alignment, place the member there, then advance by its size; every byte you skipped is padding. The struct's own alignment is the strictest alignment among its members, and at the end the cursor is rounded up to a multiple of that value, which is why sizeof frequently exceeds the end of the last member. That final rounding is not waste for its own sake: it guarantees that in T arr[n] every element starts at a properly aligned address.
Because the compiler cannot reorder, ordering is the one lever you control. Declaring members from strictest alignment down to loosest, so doubles and pointers first and then int, short, char, collapses the interior holes, because each member either already sits at a valid offset or fills the tail left by the previous one. The gain is real but bounded: it never changes the values you store, it pays off when you allocate many instances or care about cache lines, and it is off limits for a struct whose byte positions are dictated by hardware registers or an on-disk format.
<stdio.h>
<stddef.h>
struct Bad { char a; int b; char c; double d; short e; };
struct Good { double d; int b; short e; char a; char c; };
int main(void)
{
size_t payload = sizeof(char) + sizeof(int) + sizeof(char)
+ sizeof(double) + sizeof(short);
printf("Bad: size %zu align %zu\n",
sizeof(struct Bad), _Alignof(struct Bad));
printf(" a %zu b %zu c %zu d %zu e %zu\n",
offsetof(struct Bad, a), offsetof(struct Bad, b),
offsetof(struct Bad, c), offsetof(struct Bad, d),
offsetof(struct Bad, e));
printf("Good: size %zu align %zu\n",
sizeof(struct Good), _Alignof(struct Good));
printf(" d %zu b %zu e %zu a %zu c %zu\n",
offsetof(struct Good, d), offsetof(struct Good, b),
offsetof(struct Good, e), offsetof(struct Good, a),
offsetof(struct Good, c));
printf("payload %zu bytes; Bad wastes %zu, Good wastes %zu\n",
payload, sizeof(struct Bad) - payload,
sizeof(struct Good) - payload);
return 0;
}
A struct's size is decided by the order you declare its members, because each member is pushed forward to the next offset satisfying its alignment and the total is rounded up to the struct's own alignment.
Worked examples
Trailing padding exists for arrays
Shows that a struct's size is rounded up so that consecutive array elements stay aligned.
<stdio.h>
struct Pair { int n; char tag; };
int main(void)
{
struct Pair v[3] = { {1, 'a'}, {2, 'b'}, {3, 'c'} };
printf("sizeof(struct Pair) = %zu\n", sizeof(struct Pair));
printf("members need only = %zu\n", sizeof v[0].n + sizeof v[0].tag);
for (int i = 0; i < 3; i++)
printf("v[%d] starts at byte %td\n", i, (char *)&v[i] - (char *)v);
return 0;
}
Example explained
Line 1n occupies bytes 0 to 3 and tag byte 4, so the members need 5 bytes, yet sizeof reports 8.
Line 2The 3 extra bytes are trailing padding added because the struct inherits int's 4-byte alignment.
Line 3Element starts land on 0, 8, 16, so v[1].n and v[2].n are still on multiples of 4.
Line 4Casting to char * makes the subtraction count bytes, and %td is the format for the resulting ptrdiff_t.
Padding bytes break memcmp
Demonstrates that two structs with identical member values can differ byte for byte.
<stdio.h>
<string.h>
struct S { char c; int n; };
int main(void)
{
struct S a, b;
memset(&a, 0x00, sizeof a);
memset(&b, 0xFF, sizeof b);
a.c = b.c = 'x';
a.n = b.n = 42;
printf("members equal: %d\n", a.c == b.c && a.n == b.n);
printf("memcmp equal: %d\n", memcmp(&a, &b, sizeof a) == 0);
return 0;
}
Example explained
Line 1c sits at offset 0 and n at offset 4, so bytes 1 to 3 are padding the program never names.
Line 2The two memset calls fill that hole with 0x00 in a and 0xFF in b.
Line 3Assigning c and n afterwards touches only the members, leaving the padding different.
Line 4So the member comparison says equal while memcmp over all 8 bytes says different: never hash, checksum, or compare a struct as raw bytes.
Alignment propagates outward
Shows that a struct takes on the strictest alignment of its members, which then forces padding in any enclosing struct.
<stdio.h>
<stddef.h>
struct Inner { double x; };
struct Outer { char tag; struct Inner in; char flag; };
int main(void)
{
printf("align(Inner) = %zu\n", _Alignof(struct Inner));
printf("sizeof(Outer) = %zu\n", sizeof(struct Outer));
printf("offset of in = %zu\n", offsetof(struct Outer, in));
printf("offset of flag = %zu\n", offsetof(struct Outer, flag));
return 0;
}
Example explained
Line 1_Alignof(struct Inner) is 8 because a struct's alignment is the strictest alignment among its members.
Line 2tag takes byte 0, then 7 padding bytes push in up to offset 8, the next multiple of 8.
Line 3flag lands at 16, so 17 bytes are used and the size rounds up to 24.
Line 4Wrapping the double in a struct does not soften its requirement: declaring in first would give offsets 0, 8, 9 and a size of 16.
Important notes
These numbers follow the common LP64 ABI. On 32-bit i386 System V a double aligns to 4, not 8, so the same declaration measures differently; recompute per target rather than memorising sizes.
__attribute__((packed)) and #pragma pack remove the padding but leave members at unaligned addresses; taking a pointer to such a member and dereferencing it is undefined behaviour, and on some CPUs the access traps or is far slower than the bytes you saved are worth.
Common mistakes
Expecting the compiler to sort the members: C requires member addresses to increase in declaration order, so struct { char a; double b; char c; } stays 24 bytes no matter what optimisation level you use.
Comparing or hashing structs byte-wise with memcmp: padding contents are unspecified, so two objects holding identical values compare unequal and land in different hash buckets, producing bugs that appear only for some allocations.
Using sizeof(struct) as a record size for fwrite or a socket: you send the padding bytes and bake in this ABI's offsets, and a reader built with another compiler or on a 32-bit target decodes the fields at the wrong positions. Serialise member by member instead.
Try it yourself
Change, predict, then run
Declare struct A { char a; long b; char c; short d; int e; }; and print sizeof(A) plus offsetof for all five members. Then write struct B with the same five members in a different order so that sizeof(B) is exactly 16, and print its offsets to show there are no interior holes.
Open the C workspaceCheck your understanding
For struct T { char a; double b; char c; }; on an ABI where double is 8 bytes with 8-byte alignment, sizeof(struct T) is 24. Which explanation of that number is correct?
- b must start at an offset that is a multiple of 8, and the total size must be a multiple of 8 so that b stays aligned in every element of a T array
- The compiler moves b to the front and then pads each char out to a full 8 bytes
- Each member is padded up to the size of the largest member, so three members times 8 bytes gives 24
- The size of any struct is rounded up to the next power of two
Show answer
Option 0 names the two rules that produce 24: a's single byte forces 7 bytes of padding so b can sit at offset 8, then c at offset 16 leaves 17 bytes used, rounded up to 24 because the struct inherits double's 8-byte alignment. Option 2 happens to reach the same total here, but its model is wrong: for struct { char a; char c; double b; } it predicts 24 while the real size is 16, since a and c sit at offsets 0 and 1 and share a single padding gap.