Systems Programming · C and the ABI
Struct Padding and the Alignment Rule Behind sizeof
How alignment requirements, member order, and trailing padding determine a structure size, and why reordering members can shrink it without changing any field.
A structure holding a one-byte tag and a four-byte integer occupies eight bytes on every mainstream 64-bit target, not five. The extra three bytes are not a compiler bug and not an optimisation failure. They are the direct consequence of a rule that governs every object in the language: a value of type T must begin at an address that is a multiple of _Alignof(T). Because a structure’s own alignment is the maximum alignment of its members, the compiler is forced to insert holes so that each member lands on a legal boundary, and to pad the end so that arrays of the structure keep that property.
That single rule explains nearly all surprising sizeof results. It also explains why the same declaration can produce 8 bytes on one target and 12 on another, and why a header that reorders two members can change the size of every downstream binary that embeds it.
Why the hardware imposes a boundary
Alignment is not an abstract language concept layered on top of a flat address space. It comes from the memory system. A naturally aligned load or store of n bytes touches the minimum number of cache lines and can be serviced by a single access at the memory controller. An unaligned access that straddles a cache line boundary requires two accesses and a merge, and on some architectures it requires a trap and emulation in software.
The strictness varies by target, and this is where portability problems begin. x86-64 tolerates unaligned scalar loads and stores in ordinary instructions, at the cost of the split-access penalty. Many RISC architectures historically raised an alignment fault for a misaligned word access; later revisions handle ordinary loads but still require alignment for atomic and exclusive-access instructions. The language standard does not promise that unaligned access works at all, so code that relies on x86 tolerance is relying on the target, not on C.
The alignment of a scalar type is therefore a property of the ABI, not of the C standard. On the x86-64 System V ABI, char has alignment 1, short 2, int and float 4, and long, long long, double, and pointers 8. On a 32-bit x86 target, long and double are 4-byte aligned and long long is 4-byte aligned despite being 8 bytes wide. The last case is the interesting one: alignment and size are independent. A type can be wider than its alignment, and the compiler will then allow it to start at an address that does not divide its size.
Constructing the layout, member by member
The algorithm a compiler follows for an ordinary structure is small enough to state precisely. Start with offset zero. For each member in declaration order, round the current offset up to a multiple of that member’s alignment, place the member there, and advance by its size. When all members are placed, round the total up to a multiple of the structure’s alignment, which is the maximum of the members’ alignments.
#include <stddef.h>
#include <stdio.h>
struct tagged {
char tag; /* offset 0, size 1 */
int value; /* rounded up to 4, size 4 */
char flag; /* offset 8, size 1 */
};
int main(void)
{
printf("size = %zu\n", sizeof(struct tagged));
printf("value = %zu\n", offsetof(struct tagged, value));
printf("flag = %zu\n", offsetof(struct tagged, flag));
return 0;
}
The members land at 0, 4, and 8. The running total after flag is 9, and the structure’s alignment is 4 because of int, so the total rounds to 12. Three bytes of interior padding sit between tag and value; three bytes of trailing padding sit after flag.
Reordering the members changes nothing about the fields and everything about the size. Putting the two char members adjacent yields offsets 0, 1, and 4, a total of 8, and only two bytes of padding. On a 64-bit target the saving is a third of the structure. On a structure with more members the effect compounds, and the general heuristic is to sort members from widest to narrowest. That heuristic is not a guarantee, because alignment is not always equal to size, but it is a reliable first pass.
Trailing padding deserves separate attention because it is invisible in the source and expensive in arrays. The padding exists so that struct tagged a[2] keeps every element aligned: element 1 begins at offset 12, a multiple of 4. Without the rounding, element 1 would begin at 9 and its int member would be misaligned. Any structure whose last member is narrower than the structure’s alignment pays this cost on every element. A structure with a single char member has size 1 and no padding, because its alignment is 1.
What nesting and arrays do to the arithmetic
A structure member of structure type is laid out as a unit, with its own alignment and its own trailing padding already included. Nesting therefore propagates padding rather than eliminating it. A structure containing a 12-byte nested structure and a following char will place the char at offset 12, not at the offset where the nested structure’s last field ended.
Arrays inside structures are contiguous and have the element alignment, so int a[3] contributes 12 bytes at a 4-byte boundary and no interior holes. The padding around an array is entirely a matter of where the array starts and what follows it.
Unions are the other half of the picture. Every member of a union starts at offset zero, so the union’s size is the largest member size rounded up to the largest member alignment. A union of char[5] and int has size 8 on a target where int is 4-byte aligned: five bytes of payload, three of trailing padding, because the union must be able to hold an int at offset zero and every element of an array of that union must keep the alignment.
Bit-fields break the clean model. The standard leaves the allocation unit, the ordering of bits within it, and whether a bit-field may straddle a storage unit entirely to the implementation. The layout article on bitfield layout and packed structures treats that case on its own, because the rules there are conventions rather than derivations.
Where the layout stops being the compiler’s choice
Three mechanisms override the default algorithm, and each has a cost that is easy to overlook.
The first is an explicit alignment request. _Alignas in C11 and later, alignas in C++, and the older __attribute__((aligned(n))) raise a member’s or an object’s alignment above its natural value. Raising alignment only adds padding; it never removes any, and it can push a structure’s size up sharply. Raising the alignment of a large array is a common way to make it start on a page or cache-line boundary, at the cost of internal fragmentation.
The second is packing. #pragma pack(1) and __attribute__((packed)) lower the effective alignment of members to 1, which removes interior and trailing padding entirely. The resulting structure has the byte layout of a wire format or an on-disk record, which is exactly why the attribute is used. The cost is that every member access becomes a potentially unaligned access, and the compiler must generate code that either relies on the target’s tolerance or splits the access into byte loads. Taking the address of a packed member produces a pointer that may not satisfy the member type’s alignment, which the standard does not permit; the compiler typically warns and the resulting pointer is unsafe to dereference through an ordinary typed access. The full set of consequences belongs with the packed-structure discussion linked above.
The third is a change in the ABI itself. A structure that crosses a library boundary is part of that library’s binary interface, so its layout is frozen by the ABI version it was compiled against, not by the source. Adding a member, changing a member’s type, or even reordering members changes offsets, and a caller compiled against the old header will write to the wrong offsets. This is why opaque handles and accessor functions are preferred over exposed structure definitions in a stable interface. The same reasoning applies at a smaller scale to the register-classification rules in the x86-64 calling convention, where the size and alignment of a structure decide whether it is passed in registers at all.
Sizes that differ across targets
The size of a structure is not a constant even for a fixed source file. long is 8 bytes on LP64 targets and 4 on Windows and on 32-bit targets. Pointers are 8 or 4. long double is 16 bytes of storage on x86-64 Linux but 8 on Windows x64, and 16 on AArch64. _Bool is 1 byte, but the size of an enum is implementation-defined: typically int, and smaller with -fshort-enums.
The practical consequence is that sizeof results cannot be assumed across a boundary. A serialised record must be described in fixed-width types from <stdint.h>, with explicit byte order, and with the padding accounted for. That accounting is exactly the subject of integer object representation and conversion, where the byte-level view of a value becomes visible.
For introspection, offsetof from <stddef.h> gives the offset of a member as a size_t and is the portable way to assert a layout. A static assertion on the expected offsets turns a silent ABI mismatch into a compile error, which is far cheaper than discovering the mismatch through corrupted data.
#include <stddef.h>
struct header {
unsigned char version;
unsigned short length;
unsigned int checksum;
};
/* Fails to compile if the compiler lays the header out differently. */
_Static_assert(offsetof(struct header, length) == 2, "length offset");
_Static_assert(offsetof(struct header, checksum) == 4, "checksum offset");
_Static_assert(sizeof(struct header) == 8, "header size");
The assertions are the only reliable defence. A comment stating the expected size documents an intention; a static assertion enforces it on every target the code is compiled for, and fails at the point where the assumption is introduced rather than at the point where the corrupted read is observed.
References
- ISO/IEC 9899:2018 — Programming languages — C — ISO/IEC
- System V Application Binary Interface, AMD64 Architecture Processor Supplement — x86-64 psABI committee
- GCC Manual — Common Variable Attributes — GNU Project
- offsetof(3) — Linux man-pages — Linux man-pages project