Structs, unions, enums and typedef · about 40 min

Struct layout, padding and sizeof

Why this matters

You might expect a struct's size to be the sum of its members' sizes. Often it is not: the compiler inserts invisible padding bytes. This matters whenever you allocate memory for structs (always use sizeof, never add up sizes by hand), when you store millions of them (a badly ordered struct can be 50 percent larger), when you write structs to files or send them over a network (the bytes include padding), and when you compare them. Understanding layout also explains a hardware fact that shapes all low-level code: alignment.

Alignment

Processors read memory most efficiently, and on some architectures only, when a value of size N sits at an address that is a multiple of N. A 4-byte int wants an address divisible by 4, an 8-byte double an address divisible by 8. This requirement is the type's alignment. C11 added _Alignof to ask the compiler for it:

printf("alignof: char %zu, short %zu, int %zu, double %zu, pointer %zu\n",
       _Alignof(char), _Alignof(short), _Alignof(int), _Alignof(double), _Alignof(char *));
alignof: char 1, short 2, int 4, double 8, pointer 8

Why hardware cares: memory is fetched in aligned chunks (cache lines of 64 bytes, words of 8). An aligned 8-byte value always lies within one chunk. A misaligned one can straddle two, costing two fetches, and some processors (older ARM, many embedded chips) simply fault on misaligned access. On x86-64 misaligned access works but can be slower. C's rule is simple: every object must be correctly aligned for its type, and the compiler arranges that for you.

Padding inside structs

Members are laid out in declaration order; the compiler is not allowed to reorder them. To keep each member aligned, it inserts padding before members as needed, and at the end so the whole struct's size is a multiple of its own alignment (which is the largest alignment of any member).

offsetof(type, member) from <stddef.h> reports where each member starts:

#include <stdio.h>
#include <stddef.h>

struct bad {
    char flag;
    double value;
    char tag;
};

struct good {
    double value;
    char flag;
    char tag;
};

int main(void)
{
    printf("struct bad:  size %zu, flag @%zu, value @%zu, tag @%zu\n",
           sizeof(struct bad), offsetof(struct bad, flag),
           offsetof(struct bad, value), offsetof(struct bad, tag));
    printf("struct good: size %zu, value @%zu, flag @%zu, tag @%zu\n",
           sizeof(struct good), offsetof(struct good, value),
           offsetof(struct good, flag), offsetof(struct good, tag));
    struct bad arr[3];
    printf("sizeof arr = %zu\n", sizeof arr);
    return 0;
}
struct bad:  size 24, flag @0, value @8, tag @16
struct good: size 16, value @0, flag @8, tag @9
sizeof arr = 72

Drawn out, one byte per cell, P for padding:

struct bad (24 bytes, 14 of them padding)
offset  0   1   2   3   4   5   6   7   8 ...........15  16  17 ......... 23
      +---+---+---+---+---+---+---+---+-----------------+---+---------------+
      | f | P | P | P | P | P | P | P |  value (double) | t | P P P P P P P |
      +---+---+---+---+---+---+---+---+-----------------+---+---------------+
        flag   7 bytes so value is at a multiple of 8     tag  trailing padding

struct good (16 bytes, 6 of them padding)
offset  0 ............. 7   8   9  10 ............ 15
      +-------------------+---+---+-------------------+
      |  value (double)   | f | t | P P P P P P       |
      +-------------------+---+---+-------------------+

Same three members, a third less memory, just by ordering them from largest to smallest alignment.

Why trailing padding?

struct bad ends with 7 bytes of padding after tag, even though nothing follows. Consider an array: arr[1] starts at arr + sizeof(struct bad). If the size were 17, arr[1].value would be at offset 17 + 8 = 25, not a multiple of 8. Padding the size up to 24 makes every element of an array start properly aligned. That is also why sizeof arr is exactly 3 * 24 = 72: arrays never have gaps between elements, so the gap has to be inside the struct.

The ordering rule of thumb

To minimize padding, declare members in decreasing order of alignment: 8-byte members (double, long, pointers, size_t) first, then 4-byte (int, float), then 2-byte (short), then 1-byte (char, char arrays). Group small members together so they share what would otherwise be one padding gap.

Readability still matters. For a struct you create a handful of, keep the logical order that makes the code clearest. Reorder when you store many of them, or when a profiler says memory is the problem.

gcc can show you every padding gap with -Wpadded (not part of -Wall; it is purely informational):

p1.c:6:12: warning: padding struct to align 'value' [-Wpadded]
p1.c:8:1: warning: padding struct size to alignment boundary with 7 bytes [-Wpadded]

Consequences you will meet

Common mistakes

Recap

Going further: predict, then check with a small program, the size of struct { char c; long l; char d; int i; } (it is 24; can you get it to 16?). Then add -Wpadded to see gcc explain each gap.

Exercises

Exercise 1 · quiz

How big is it?

On x86-64 Linux (int is 4 bytes and 4-aligned), what is sizeof this struct?

struct s {
    char c;
    int i;
    char d;
};
Answer
  • 12

c at offset 0, then 3 bytes of padding so i starts at offset 4 (a multiple of its alignment), i occupies 4-7, d at offset 8, then 3 bytes of trailing padding so the size (12) is a multiple of the struct's alignment (4). Trailing padding makes the next element of an array of these structs start correctly aligned. Moving d next to c gives 8.

Exercise 2 · quiz

Work it out yourself

What is sizeof(struct t) on x86-64? (short is 2 bytes and 2-aligned.) Answer with a number.

struct t {
    char a;
    short b;
    char c;
};
Answer

a at 0, one byte of padding, b at 2-3, c at 4, then one byte of trailing padding because the struct's alignment is 2 (that of its most-aligned member, short). Total 6. Reordered as short, char, char it would be 4 with no padding at all.

Exercise 3 · write code

Shrink the record

A program stores millions of these records, and each one wastes space on padding:

struct record {
    char kind;
    double price;
    short qty;
    char flag;
    int id;
    char *name;
};

Reorder the members (keep every member, with the same name and type) so that the struct is as small as possible. The program prints its size; make it print exactly:

size 24

The check also verifies that all six members are still there with their original types.

File: main.c

Starting point

#include <stdio.h>

struct record {
    char kind;
    double price;
    short qty;
    char flag;
    int id;
    char *name;
};

int main(void)
{
    printf("size %zu\n", sizeof(struct record));
    return 0;
}

Example

expected output

size 24
Hint 1

Add up the member sizes: 1 + 8 + 2 + 1 + 4 + 8 = 24. Any padding is waste.

Hint 2

Order members from the largest alignment to the smallest: the 8-byte ones first, then int, then short, then the chars.

Reference solution
#include <stdio.h>

struct record {
    double price;
    char *name;
    int id;
    short qty;
    char kind;
    char flag;
};

int main(void)
{
    printf("size %zu\n", sizeof(struct record));
    return 0;
}

Exercise 4 · quiz

Why not compare structs with memcmp?

Someone compares two struct s { char c; int i; } values with memcmp(&a, &b, sizeof a) == 0. Both have c == 'x' and i == 5, yet the comparison sometimes says they differ. Why?

Answer
  • The 3 padding bytes after c are not set by member assignments and can hold different leftover values

memcmp compares every byte, padding included, and padding has unspecified contents: assigning members never touches it, and copying may or may not. Two structs equal member by member can differ in their padding. Compare members explicitly. (Zeroing both structs first with memset or = {0} usually works in practice, but it is fragile.)