Dynamic Structs and Memory Layout

You know how to create structs. But so far, you’ve been creating them like a packed suitcase - decided ahead of time, fixed size, always the same.

What if you need to create structs while your program is running? What if you don’t know how many you’ll need until someone tells you?

Time to learn dynamic struct allocation.

Creating Structs with malloc

You can use malloc to create structs at runtime:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char name[50];
    int age;
} Person;

int main(void) {
    // Allocate space for one Person
    Person *p = malloc(sizeof(Person));

    if (p == NULL) {
        printf("Couldn't allocate memory!\n");
        return 1;
    }

    // Use -> because p is a pointer
    strcpy(p->name, "Alice Smith");
    p->age = 17;

    printf("%s is %d years old\n", p->name, p->age);

    free(p);  // Don't forget!
    return 0;
}

Remember: when you have a pointer to a struct, use -> instead of . to access fields.

Arrays of Dynamic Structs

Need a bunch of structs? Allocate an array:

int count;
printf("How many heroes? ");
scanf("%d", &count);

// Allocate array of Person structs
Person *heroes = malloc(count * sizeof(Person));

if (heroes == NULL) {
    printf("Out of memory!\n");
    return 1;
}

// Use like a normal array
for (int i = 0; i < count; i++) {
    printf("Hero %d name: ", i + 1);
    scanf("%49s", heroes[i].name);
    heroes[i].age = 25;  // Default age
}

// Print them
for (int i = 0; i < count; i++) {
    printf("%s\n", heroes[i].name);
}

free(heroes);  // Free the whole array

Structs with Pointer Members

Sometimes a struct contains a pointer to other memory. Think of a library card catalog - each card (struct) contains a reference to where the actual book is stored (dynamically allocated data).

typedef struct {
    char *name;  // Pointer to dynamically allocated string
    int power_level;
} Hero;

Why use a pointer instead of a fixed array like char name[50]?

  • Names can be any length (not limited to 50 characters)
  • Uses only the memory you need
  • More flexible

But it requires more care:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char *name;
    int power_level;
} Hero;

Hero *create_hero(const char *name, int power) {
    // Allocate the struct
    Hero *h = malloc(sizeof(Hero));
    if (h == NULL) return NULL;

    // Allocate space for the name (+1 for null terminator)
    h->name = malloc(strlen(name) + 1);
    if (h->name == NULL) {
        free(h);  // Clean up if second malloc fails!
        return NULL;
    }

    // Copy the data
    strcpy(h->name, name);
    h->power_level = power;
    return h;
}

void destroy_hero(Hero *h) {
    if (h != NULL) {
        free(h->name);  // Free the name FIRST
        free(h);        // THEN free the struct
    }
}

int main(void) {
    Hero *hero = create_hero("Gandalf", 9001);
    if (hero == NULL) {
        return 1;
    }

    printf("%s has power level %d\n", hero->name, hero->power_level);

    destroy_hero(hero);
    return 0;
}

The Order Matters!

When freeing a struct with pointer members:

  1. Free the members first
  2. Then free the struct

If you do it backwards, you lose access to the pointers inside. It’s like demolishing a parking garage before driving the cars out - now you can’t reach them!

// WRONG - memory leak!
free(h);        // Struct is gone
free(h->name);  // Can't access h->name anymore!

// RIGHT
free(h->name);  // Free what's inside first
free(h);        // Then free the container

Struct Memory Layout and Padding

Here’s something that surprises people: structs sometimes use more memory than you’d expect.

typedef struct {
    char a;    // 1 byte
    int b;     // 4 bytes
    char c;    // 1 byte
} Example;

printf("Size: %zu\n", sizeof(Example));  // Probably 12, not 6!

Why 12 instead of 6?

The computer adds empty space called padding. It’s like how a shipping company puts foam peanuts in a box - the actual items are smaller than the box, but the padding makes handling easier.

Computers read memory fastest when data is “aligned” - when 4-byte numbers are at addresses divisible by 4. The padding makes this happen. In this example, a 1-byte char is followed by 3 bytes of padding, then a 4-byte int, then another 1-byte char followed by 3 more bytes of padding - totaling 12 bytes instead of the expected 6.

Memory layout showing 12 bytes in a row: byte 1 contains char 'a', bytes 2-4 are padding (shown as dots), bytes 5-8 contain the int 'b', byte 9 contains char 'c', and bytes 10-12 are padding. Labels below show which bytes belong to each variable and which are padding.

The dots are wasted space. Empty. Like filler issues in a comic book series.

Reducing Padding

Order your fields from largest to smallest:

// Wasteful order
typedef struct {
    char a;    // 1 byte + 3 padding
    int b;     // 4 bytes
    char c;    // 1 byte + 3 padding
} Wasteful;    // Total: 12 bytes

// Better order
typedef struct {
    int b;     // 4 bytes
    char a;    // 1 byte
    char c;    // 1 byte + 2 padding
} Better;      // Total: 8 bytes

This matters when you have millions of structs (like in a game with lots of enemies) or when saving data to files.

Resizing Dynamic Arrays

What if you need to grow an array? Use realloc:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int capacity = 2;
    int count = 0;

    int *numbers = malloc(capacity * sizeof(int));

    // Keep adding numbers
    for (int i = 0; i < 10; i++) {
        // Need more space?
        if (count >= capacity) {
            capacity *= 2;  // Double the size
            int *temp = realloc(numbers, capacity * sizeof(int));
            if (temp == NULL) {
                printf("Out of memory!\n");
                free(numbers);
                return 1;
            }
            numbers = temp;
            printf("Grew to capacity %d\n", capacity);
        }

        numbers[count] = i * 10;
        count++;
    }

    // Print them
    for (int i = 0; i < count; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");

    free(numbers);
    return 0;
}

realloc either:

  • Extends your existing memory (if there’s room)
  • Allocates new memory, copies your data, and frees the old memory

Either way, you get a pointer to memory with the new size.

Important: Always use a temporary variable with realloc. If it fails and returns NULL, you don’t want to lose your original pointer!

Complete Example: Dynamic Hero Roster

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char *name;
    char *power;
    int strength;
} Hero;

typedef struct {
    Hero *heroes;
    int count;
    int capacity;
} Roster;

void roster_init(Roster *r) {
    r->heroes = NULL;
    r->count = 0;
    r->capacity = 0;
}

int roster_add(Roster *r, const char *name, const char *power, int strength) {
    // Need more space?
    if (r->count >= r->capacity) {
        int new_cap = (r->capacity == 0) ? 4 : r->capacity * 2;
        Hero *temp = realloc(r->heroes, new_cap * sizeof(Hero));
        if (temp == NULL) return 0;
        r->heroes = temp;
        r->capacity = new_cap;
    }

    // Create the hero
    Hero *h = &r->heroes[r->count];
    h->name = malloc(strlen(name) + 1);
    h->power = malloc(strlen(power) + 1);
    if (h->name == NULL || h->power == NULL) {
        free(h->name);
        free(h->power);
        return 0;
    }

    strcpy(h->name, name);
    strcpy(h->power, power);
    h->strength = strength;
    r->count++;
    return 1;
}

void roster_print(Roster *r) {
    printf("\n=== HERO ROSTER ===\n");
    for (int i = 0; i < r->count; i++) {
        printf("%s - %s (Strength: %d)\n",
               r->heroes[i].name,
               r->heroes[i].power,
               r->heroes[i].strength);
    }
    printf("Total: %d heroes\n", r->count);
}

void roster_free(Roster *r) {
    for (int i = 0; i < r->count; i++) {
        free(r->heroes[i].name);
        free(r->heroes[i].power);
    }
    free(r->heroes);
    r->heroes = NULL;
    r->count = 0;
    r->capacity = 0;
}

int main(void) {
    Roster team;
    roster_init(&team);

    roster_add(&team, "Ada Lovelace", "Programming", 95);
    roster_add(&team, "Alan Turing", "Codebreaking", 98);
    roster_add(&team, "Grace Hopper", "Compilers", 92);
    roster_add(&team, "Dennis Ritchie", "C Language", 99);
    roster_add(&team, "Linus Torvalds", "Linux", 94);

    roster_print(&team);

    roster_free(&team);
    return 0;
}

Try It Yourself

  1. Create a Book struct with dynamic title and author strings. Write create and destroy functions.
  2. Modify the hero roster to support removing heroes
  3. Add a function to find the strongest hero in the roster
  4. What happens if you forget to free a hero’s name before freeing the hero? (Memory leak!)

Common Mistakes

  • Freeing struct members in wrong order (losing access to pointers)
  • Forgetting to check malloc/realloc return values
  • Not initializing pointer members to NULL
  • Losing the original pointer when realloc fails

Next Up

In Part 15, we’ll learn about function pointers - storing functions in variables and passing them as arguments.


Enjoyed This?

If this helped something click, subscribe to my YouTube channel. More content like this, same approach - making things stick without insulting your intelligence. It’s free, it helps more people find this stuff, and it tells me what’s worth making more of.