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.
| Type | Guarantee | Use for |
|---|---|---|
int8_t … int64_t | Exactly that many bits, if the platform provides them | Binary formats, hashing, bit manipulation |
int_fast32_t | At least 32 bits, fastest available | Arithmetic where speed matters more than width |
int_least16_t | At least 16 bits, smallest available | Compact stored data |
size_t | Unsigned, large enough for any object size | Lengths, indices, loop counters over memory |
ptrdiff_t | Signed difference of two pointers | Pointer subtraction, which may be negative |
intptr_t / uintptr_t | Holds a converted pointer | Storing a pointer in an integer |
long long | At least 64 bits | Portable 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
%zuforsize_t.%dtruncates on 64-bit systems and the compiler will warn. - Structure padding differs between ABIs. Use
offsetofand explicit serialisation for anything that crosses a boundary, and#pragma packonly 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
| Category | Meaning | Example |
|---|---|---|
| Undefined behaviour | The standard imposes no requirements; the optimiser may assume it never happens | Signed overflow, out-of-bounds access, use after free |
| Unspecified behaviour | The implementation chooses, and need not document or be consistent | Evaluation order of function arguments |
| Implementation-defined | The implementation chooses and must document the choice | sizeof(int), right shift of a negative value, char signedness |
| Constraint violation | The program is not conforming; a compiler must diagnose it | Assigning 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.Related
Structs, files and undefined behaviour Debugging with gdb, sanitizers and valgrind
Last refreshed 2026-09-18.