The C Path — learn C, visually

🚀 Modern C (C11 → C23)

Alignment: why your struct is bigger than its parts

⏱ 13 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

Ask C for a struct whose members add up to 6 bytes and sizeof reports 12 — half of it invisible empty space. In this lesson you find out where those hidden bytes go, and you pick up a 60-second member-reordering trick that can cut a program's memory use by a third when it stores millions of objects (game entities, chat messages, log records).

Add up the members of struct { char a; int b; char c; }: 1 + 4 + 1 = 6 bytes. Ask sizeof and you get 12. Where did 6 bytes go? The answer is alignment — one of those invisible rules that quietly shapes every byte of memory your program touches.

What alignment is

Hardware doesn't read memory one byte at a time; it moves fixed-size chunks (4, 8, 16 bytes) that start at addresses divisible by their size. A type's alignment requirement says which addresses it may live at: a 4-byte int wants an address divisible by 4, an 8-byte double one divisible by 8.

This spot has an interactive memgrid widget — open the interactive lesson to play with it.

This spot has an interactive memgrid widget — open the interactive lesson to play with it.

Why care? Three big reasons. Speed: a misaligned value can straddle two hardware chunks — two loads plus stitching instead of one. Correctness: some CPUs (older ARM, SPARC) flat-out fault on misaligned access, and in C, accessing a misaligned object is undefined behavior anyway. Special instructions: atomics and SIMD vector loads often require alignment — a 16-byte SSE load from a non-16-byte address crashes.

🧠 Checkpoint: A type with alignment 8 may live at which addresses?

  • Any address
  • Only even addresses
  • Addresses divisible by 8
  • Addresses ending in 8
Show answer

Addresses divisible by 8 — Alignment N means the address must be a multiple of N. 0x1000, 0x1008, 0x7ffe10 are fine for alignment 8; 0x1004 is not.

Querying: alignof

C11 added the keyword _Alignof (with a friendly alignof macro in <stdalign.h>; in C23 alignof is simply a keyword). It tells you any type's requirement:

alignof.c
#include <stdio.h>
#include <stdalign.h>   /* alignof / alignas macros (C11–C17)  */
#include <stddef.h>     /* max_align_t                          */

struct mix { char c; double d; };

int main(void) {
    printf("char        : size %2zu  align %2zu\n", sizeof(char),        alignof(char));
    printf("short       : size %2zu  align %2zu\n", sizeof(short),       alignof(short));
    printf("int         : size %2zu  align %2zu\n", sizeof(int),         alignof(int));
    printf("double      : size %2zu  align %2zu\n", sizeof(double),      alignof(double));
    printf("struct mix  : size %2zu  align %2zu\n", sizeof(struct mix),  alignof(struct mix));
    printf("max_align_t : size %2zu  align %2zu\n", sizeof(max_align_t), alignof(max_align_t));
    return 0;
}
terminal
$ gcc -std=c17 alignof.c -o alignof && ./alignof
char        : size  1  align  1
short       : size  2  align  2
int         : size  4  align  4
double      : size  8  align  8
struct mix  : size 16  align  8
max_align_t : size 32  align 16
# a struct inherits the STRICTEST alignment among its members
# (values are typical x86-64 Linux — alignment is implementation-defined)

max_align_t (from <stddef.h>) has the strictest alignment of any standard type — and malloc guarantees its results are aligned for it. That's why you can malloc anything without thinking about alignment.

Struct padding — and the reordering trick

Now the mystery solves itself. Inside a struct, every member must land on its own aligned offset, so the compiler inserts invisible padding bytes. And the struct's total size must be a multiple of its largest member's alignment (so arrays of it stay aligned). Here's our 12-byte struct:

This spot has an interactive memgrid widget — open the interactive lesson to play with it.

But padding depends on order, and order is yours to choose. Sort members largest-first and the holes vanish:

This spot has an interactive memgrid widget — open the interactive lesson to play with it.

💡

Free memory optimization: ordering struct members from largest to smallest alignment never loses, and often shrinks the struct by 30–50%. For a struct allocated a million times, that's real megabytes. Confirm your layout with offsetof from <stddef.h> — and lock it in with a static_assert on sizeof!

🧠 Checkpoint: Why does the compiler pad struct { char a; int b; } to 8 bytes instead of 5?

  • To leave room for future members
  • So b starts at offset 4, and array elements stay aligned
  • Because sizeof must be a power of two
  • It’s a gcc bug you can disable
Show answer

So b starts at offset 4, and array elements stay aligned — b needs a 4-divisible offset, so 3 pad bytes go after a. Tail padding then rounds the size to a multiple of the struct’s alignment so element [1] of an array is aligned too. (sizeof being a power of two is a coincidence here — a struct of 3 ints is 12.)

Forcing it: alignas and aligned_alloc

Sometimes the natural alignment isn't enough. Two threads hammering variables that share a 64-byte cache line will slow each other down ("false sharing"); SIMD code wants 16- or 32-byte buffers. C11's _Alignas / alignas over-aligns an object, and aligned_alloc does the same for the heap:

alignas.c
#include <stdio.h>
#include <stdlib.h>
#include <stdalign.h>

/* keep two hot counters on SEPARATE 64-byte cache lines */
struct counters {
    alignas(64) long a;
    alignas(64) long b;
};

int main(void) {
    alignas(16) unsigned char simd_buf[64];   /* SSE-load ready */

    struct counters c;
    printf("&c.a = %p\n&c.b = %p\n", (void *)&c.a, (void *)&c.b);
    printf("sizeof(struct counters) = %zu\n", sizeof(struct counters));
    printf("simd_buf %% 16 == %lu\n", (unsigned long)simd_buf % 16);

    /* heap version: 32-byte-aligned block (size = multiple of align!) */
    double *v = aligned_alloc(32, 8 * sizeof(double));
    printf("v        %% 32 == %lu\n", (unsigned long)(void *)v % 32);
    free(v);
    return 0;
}
terminal
$ gcc -std=c17 alignas.c -o alignas && ./alignas
&c.a = 0x7ffc5a3c1e80
&c.b = 0x7ffc5a3c1ec0
sizeof(struct counters) = 128
simd_buf % 16 == 0
v        % 32 == 0
# 0x...e80 and 0x...ec0 are 64 apart — each counter owns its cache line
⚠️

Rules: alignas can only increase alignment (you can't ask for less than the type needs), and the value must be a power of two. For aligned_alloc(align, size), keep size a multiple of align — that's the portable contract — and free with plain free().

This spot has an interactive editor widget — open the interactive lesson to play with it.

🧠 Checkpoint: What does malloc guarantee about alignment?

  • Nothing — use aligned_alloc always
  • Results are aligned for max_align_t, enough for any standard type
  • Results are always 4096-byte aligned
  • Alignment equal to the requested size
Show answer

Results are aligned for max_align_t, enough for any standard type — malloc returns memory suitably aligned for ANY object with fundamental alignment — i.e. aligned to alignof(max_align_t) (16 on typical x86-64). You only need aligned_alloc for OVER-alignment, like 32-byte AVX buffers or page-aligned I/O.

Alignment is about where values live; next we tackle a C11 feature about which code runs for each type — _Generic, C's answer to overloading.

▶ Practice this lesson interactively (with live gcc)