Memory Model and Alignment
|
This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
C exposes memory as one uniform, byte-addressable space: every object occupies a contiguous sequence of bytes, and those bytes can be examined directly. What C then layers on top of that — effective types and the strict aliasing rule — is what lets the optimizer work, and is the part most easily violated by accident.
The Uniform Memory Model
Every object has:
-
A size in bytes (
sizeof), a byte beingCHAR_BITbits (8 in practice). -
An address (
&), except for bit-fields and objects declaredregister. -
An alignment: the address must be a multiple of it.
-
An object representation: the
sizeof(T)bytes that encode its value.
The bytes of any object may be inspected through a pointer to unsigned char — this is one of the very few
type-punning operations the standard blesses outright:
#include <stdio.h>
int main(void)
{
double value = 1.0;
const unsigned char *bytes = (const unsigned char *)&value;
printf("sizeof(double) = %zu, bytes:", sizeof value);
for (size_t i = 0; i < sizeof value; ++i) {
printf(" %02X", bytes[i]); // 00 00 00 00 00 00 F0 3F on a little-endian x86
}
putchar('\n');
return 0;
}
unsigned char is special because it has no padding bits, no trap representations, and is explicitly permitted
to alias any object type. char and signed char are not interchangeable with it for this purpose — use
unsigned char for raw bytes, always.
Byte order is not specified by C. That same loop is how you discover it, and why any format that leaves the process needs explicit serialization:
#include <stdint.h>
#include <stdio.h>
static bool is_little_endian(void)
{
const uint16_t probe = 0x0102u;
return *(const unsigned char *)&probe == 0x02u;
}
// Portable serialization: build the bytes yourself, no casting involved.
static void store_be32(unsigned char out[4], uint32_t value)
{
out[0] = (unsigned char)(value >> 24);
out[1] = (unsigned char)(value >> 16);
out[2] = (unsigned char)(value >> 8);
out[3] = (unsigned char)value;
}
static uint32_t load_be32(const unsigned char in[4])
{
return (uint32_t)in[0] << 24 | (uint32_t)in[1] << 16
| (uint32_t)in[2] << 8 | (uint32_t)in[3];
}
int main(void)
{
unsigned char wire[4];
store_be32(wire, 0xDEADBEEFu);
printf("%s, round trip = %08X\n",
is_little_endian() ? "little-endian" : "big-endian", load_be32(wire));
return 0;
}
C23 adds <stdbit.h> with __STDC_ENDIAN_NATIVE__, __STDC_ENDIAN_LITTLE__ and
__STDC_ENDIAN_BIG__ so endianness can finally be tested at compile time.
Effective Types and Strict Aliasing
The strict aliasing rule: an object’s stored value may only be accessed through an lvalue of a compatible type — with the exceptions of a signed/unsigned variant of it, a qualified version of it, an aggregate containing it, or a character type.
The rule exists so the compiler can assume that a write through an int * cannot change what a float *
points at, and therefore keep values in registers. Violating it produces code that works at -O0 and breaks
at -O2:
static float bad_bit_cast(int bits)
{
return *(float *)&bits; // UNDEFINED: reads an int object through a float lvalue
}
warning: dereferencing type-punned pointer will break strict-aliasing rules
[-Wstrict-aliasing]
There are exactly three correct ways to reinterpret bytes.
1. memcpy — Always Correct
#include <stdint.h>
#include <stdio.h>
#include <string.h>
static float bits_to_float(uint32_t bits)
{
float result;
static_assert(sizeof result == sizeof bits, "float must be 32 bits here");
memcpy(&result, &bits, sizeof result); // no aliasing violation, and free at -O2
return result;
}
static uint32_t float_to_bits(float value)
{
uint32_t bits;
memcpy(&bits, &value, sizeof bits);
return bits;
}
int main(void)
{
printf("%g <-> %08X\n", bits_to_float(0x3F800000u), float_to_bits(1.0f));
return 0;
}
Every mainstream compiler turns a memcpy of a register-sized object into a single move at -O1 and above — this is the idiom to reach for, not a fallback.
2. A union — Permitted, Even Reading a Member Other Than the Last Written
Writing one member and reading another is implementation-defined rather than undefined in C (unlike C++), and every mainstream compiler documents it as working:
#include <stdint.h>
#include <stdio.h>
union FloatBits {
float as_float;
uint32_t as_bits;
};
int main(void)
{
union FloatBits u = { .as_float = 1.0f };
printf("%08X\n", u.as_bits); // 3F800000 -- fine through a union
return 0;
}
The catch is that this only works when the punning happens through the union object itself; taking a
float * into a union member and using it elsewhere brings the aliasing rule back.
Explicit Pointer Conversions
#include <stdint.h>
#include <stdio.h>
struct Header { uint16_t magic; uint16_t length; };
int main(void)
{
// Object pointer <-> void *: implicit, always safe.
struct Header h = { .magic = 0xC0DEu, .length = 8u };
void *opaque = &h;
struct Header *back = opaque;
// Object pointer <-> integer: use uintptr_t, the only type guaranteed to fit.
uintptr_t as_int = (uintptr_t)&h;
struct Header *round_tripped = (struct Header *)as_int;
printf("%04X %u %d\n", back->magic, round_tripped->length, back == round_tripped);
return 0;
}
What is not safe: converting between incompatible object pointer types and then dereferencing (the aliasing
rule above), and converting to a type with stricter alignment than the object has. Casting a
unsigned char * from the middle of a buffer to uint32_t * and dereferencing it is undefined on both counts — misaligned access is a crash on some architectures and a silent slowdown on x86. memcpy out of the buffer
instead.
Function pointers are a separate universe: converting one function-pointer type to another and calling it is
undefined, and there is no guarantee a function pointer fits in void * (POSIX requires it, C does not).
Alignment
#include <stdalign.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
struct Vec4 { float v[4]; };
int main(void)
{
printf("alignof(char)=%zu int=%zu double=%zu max_align_t=%zu\n",
alignof(char), alignof(int), alignof(double), alignof(max_align_t));
// C11/C23: over-align an object -- e.g. to a cache line or for SIMD.
alignas(64) unsigned char cache_line[64] = { 0 };
alignas(16) struct Vec4 simd = { .v = { 1.0f, 2.0f, 3.0f, 4.0f } };
// Over-aligned dynamic memory: size must be a multiple of the alignment.
void *block = aligned_alloc(64, 128);
if (block == nullptr) {
return EXIT_FAILURE;
}
printf("cache_line %% 64 = %zu, simd %% 16 = %zu, block %% 64 = %zu\n",
(uintptr_t)cache_line % 64u, (uintptr_t)&simd % 16u, (uintptr_t)block % 64u);
free(block); // aligned_alloc memory is freed with plain free
return 0;
}
The rules worth committing to memory:
-
Every type’s alignment is a power of two dividing its size;
max_align_t’s alignment is the strictest any standard type needs, and it is what `mallocguarantees. -
malloc/calloc/reallocreturn memory suitably aligned for any object type — you never needaligned_allocunless you need over-alignment (SIMD, cache lines, DMA). -
alignasmay only increase alignment (alignas(1) intis an error), and applies to declarations, not expressions. C11 spelled it_Alignas. -
Under-aligned access is undefined behavior:
-fsanitize=alignment(part of-fsanitize=undefined) catches it.
Over-alignment is also how you avoid false sharing: two atomics on the same cache line serialize even though the code never shares them. See Atomics and Memory Consistency.
See Also
-
Structures, Unions and Type Aliases — padding,
offsetofand bit-fields. -
Dynamic Memory Allocation —
aligned_allocand whatmallocguarantees. -
Pointers —
restrict,void *and pointer conversions. -
Performance — why the aliasing rule earns its keep.
-
C++: Memory Management and Smart Pointers — C++ shares this object and alignment model, and adds
alignas/alignofplus allocator-aware containers.
References
-
WG14 N3220 — the C23 working draft (§6.2.6 "Representations of types", §6.5 para. 7 "the effective type rule", §6.2.8 "Alignment of objects", §6.7.6 "Alignment specifier").
-
cppreference.com — Type (compatible and effective types).
-
GCC manual — Optimize Options (
-fstrict-aliasing,-fno-strict-aliasing). -
GCC manual — Structures, unions, enumerations, and bit-fields implementation (union type punning).
-
Clang — UndefinedBehaviorSanitizer (
alignment,type-punningchecks).