← C for People Who Already Know How to Code

C for People Who Already Know How to Code

Structs, Padding and Alignment

The compiler pads your struct behind your back

Every type has an alignment requirement: the addresses it's allowed to start at. The compiler inserts padding bytes between (and sometimes after) struct members so each one lands on a legal address for its type, and so arrays of the struct keep every element aligned too. sizeof(struct) is essentially never just the sum of its members' sizes.

Worst-to-best order · wastes 6 bytes

char a  (1 byte)
padding  (3 bytes)
int b  (4 bytes)
char c  (1 byte)
padding  (3 bytes)
sizeof(struct) = 12

Fields reordered largest-first

int b  (4 bytes)
char a  (1 byte)
char c  (1 byte)
padding  (2 bytes)
sizeof(struct) = 8

Same three fields, same total useful data, four fewer bytes just from ordering members largest-alignment-first. This isn't a micro-optimization exclusive to embedded work. A struct like this repeated across a large array is the difference between fitting in cache and not.

c
struct bad  { char a; int b; char c; };  // sizeof == 12 on a typical 64-bit target
struct good { int b; char a; char c; };  // sizeof == 8: same fields, better order

printf("%zu vs %zu\n", sizeof(struct bad), sizeof(struct good));
Try it yourself: measure it on your own struct

Take any struct in a project you actually work on with more than two fields, printf("%zu\n", sizeof(...)) it, then reorder fields largest-alignment-first and compare. It's a five-minute change with a real, measurable effect.

Alignment requirements, and forcing them with _Alignas

<stdalign.h> (C11) gives you alignof(T) to query a type's natural alignment and _Alignas(N) to force a stricter one than the compiler would otherwise pick. You reach for this rarely: SIMD buffers that need 16/32-byte alignment, DMA buffers with hardware alignment requirements, or lock-free structures that need to land on their own cache line to avoid false sharing between threads.

Flexible array members beat the old "struct hack"

C99 standardized a trailing unsized array as the last struct member: the idiomatic way to allocate a header plus a variable-length payload in one contiguous block, replacing the old undefined-behavior-adjacent int data[1] trick:

c
struct packet {
	size_t len;
	unsigned char data[]; // flexible array member, must be last, contributes 0 to sizeof(struct packet)
};

struct packet *pk = malloc(sizeof(struct packet) + payload_len);
pk->len = payload_len;
memcpy(pk->data, payload, payload_len);

Bitfields: real memory savings, implementation-defined layout

unsigned flag1 : 1; unsigned flag2 : 1; packs multiple small fields into shared storage, but the standard does not pin down bit order, padding between fields, or whether they pack across a byte boundary. Two different compilers (or the same compiler on two architectures) are allowed to lay the same bitfield struct out differently. Fine for in-process flags you never serialize; dangerous the moment that struct's raw bytes cross a compiler boundary: a network protocol, a file format, a different build target.