Portability, standards and disciplined C

Fixed-width types, endianness and alignment, the difference between undefined and unspecified behaviour, and warnings as errors.

Sizes, fixed-width types and alignment

The standard does not fix the size of int or long. It fixes minimum ranges, so int is 16, 32 or 64 bits depending on the platform. When the width matters, say so in the type.

TypeGuaranteeUse for
int8_tint64_tExactly that many bits, if the platform provides themBinary formats, hashing, bit manipulation
int_fast32_tAt least 32 bits, fastest availableArithmetic where speed matters more than width
int_least16_tAt least 16 bits, smallest availableCompact stored data
size_tUnsigned, large enough for any object sizeLengths, indices, loop counters over memory
ptrdiff_tSigned difference of two pointersPointer subtraction, which may be negative
intptr_t / uintptr_tHolds a converted pointerStoring a pointer in an integer
long longAt least 64 bitsPortable 64-bit arithmetic without <stdint.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

_Static_assert(sizeof(int32_t) == 4, "int32_t must be exactly 4 bytes");

/* serialise explicitly: do not memcpy a struct over the wire */
void put_u32be(uint8_t *dst, uint32_t v) {
    dst[0] = (uint8_t)(v >> 24);
    dst[1] = (uint8_t)(v >> 16);
    dst[2] = (uint8_t)(v >>  8);
    dst[3] = (uint8_t)(v);
}

uint32_t get_u32be(const uint8_t *src) {
    return ((uint32_t)src[0] << 24) | ((uint32_t)src[1] << 16)
         | ((uint32_t)src[2] <<  8) |  (uint32_t)src[3];
}

int main(void) {
    uint8_t buf[4];
    put_u32be(buf, 0x01020304u);
    printf("%08x\n", get_u32be(buf));   /* 01020304 on every platform */
    return 0;
}
  • Use %zu for size_t. %d truncates on 64-bit systems and the compiler will warn.
  • Structure padding differs between ABIs. Use offsetof and explicit serialisation for anything that crosses a boundary, and #pragma pack only when a documented format demands it.
  • Alignment matters: a misaligned access may be slow on x86 and a fatal fault on some ARM configurations. alignas (C23) and _Alignas (C11) request it explicitly.

Undefined, unspecified and implementation-defined

CategoryMeaningExample
Undefined behaviourThe standard imposes no requirements; the optimiser may assume it never happensSigned overflow, out-of-bounds access, use after free
Unspecified behaviourThe implementation chooses, and need not document or be consistentEvaluation order of function arguments
Implementation-definedThe implementation chooses and must document the choicesizeof(int), right shift of a negative value, char signedness
Constraint violationThe program is not conforming; a compiler must diagnose itAssigning a pointer to an int without a cast
/* all of these are undefined behaviour */
int x = INT_MAX + 1;             /* signed overflow */
int a[4]; a[4] = 0;              /* one past the end */
p = malloc(8); free(p); *p = 1;  /* use after free */
i = i++ + 1;                     /* two unsequenced modifications */
shift = 1 << 32;                 /* shift >= the width of the type */
memcpy(dst, dst + 1, 10);        /* overlapping regions */

/* and these are merely unspecified, not UB */
printf("%d %d\n", f(), g());     /* argument evaluation order is unspecified */

Compilers exploit undefined behaviour aggressively. A loop with signed overflow can be deleted entirely, and a NULL check after a dereference can be removed because the dereference already assumed non-NULL. That is why UB looks like the optimiser "changing your logic".

Making the compiler your reviewer

CFLAGS := -std=c17 -O2 -g \
          -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion \
          -Wshadow -Wcast-qual -Wstrict-prototypes -Wmissing-prototypes \
          -Wformat=2 -Wundef -Wvla -Wwrite-strings -Werror

# extra hardening for shipped binaries
cc $(CFLAGS) -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE -pie app.c -o app

# static analysis
clang --analyze app.c
cppcheck --enable=all --inconclusive app.c
⚠️
Undefined behaviour is not "whatever the hardware does". It is a licence for the optimiser to assume the situation is impossible, which can turn one bad line into code that misbehaves somewhere else entirely — sometimes in a build that has no debugging information at all.

FAQ

Is unsigned overflow defined?
Yes. Unsigned arithmetic wraps modulo 2 to the power of the width, which is why hash functions rely on it. Signed overflow is undefined and should be avoided or checked.
Should I enable -ffast-math or -fno-strict-aliasing?
Only with a documented reason. Both relax rules the standard defines, and -ffast-math changes results for NaN and infinity handling. Better to restructure the code than to switch off the guarantees.

Structs, files and undefined behaviour Debugging with gdb, sanitizers and valgrind

Last refreshed 2026-09-18.