Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 41

Dynamic Inventory with Dynamic Memory

Recommended resources

Introduction

In our last lesson, we explored the theory behind realloc(), C's function for resizing dynamically allocated memory. You learned about its two main outcomes—in-place expansion and relocation—and, most importantly, the safe pattern for using it with a temporary pointer to prevent memory leaks and crashes.

Today, we move from theory to practice. We will take the static, array-based inventory we developed in Module 6 and refactor it into a truly dynamic system. This is a significant milestone, as it allows our game's inventory to start empty and grow to any size, limited only by available memory. This process of converting a fixed-size data structure to a dynamic one is a fundamental task in systems programming and is essential for building flexible and robust game engines.


The Architectural Shift: From Array to Pointer

Let's begin by revisiting the structure of our old inventory. It was likely defined within a player struct or as a standalone global, using a preprocessor directive to fix its size:

// The old, static way
#define INVENTORY_CAPACITY 10

struct Item {
    // ... item members like name, weight, etc.
};

struct Player {
    // ... other player stats
    struct Item inventory[INVENTORY_CAPACITY];
    int num_items;
};

This design is simple but rigid. To make it dynamic, we need to change its architecture. Instead of embedding the array of items directly into another structure, we will store a pointer to a block of memory on the heap. This requires two new pieces of information: the pointer itself and a variable to track the capacity of the allocated block.

Our new Inventory structure will look like this:

struct Item {
    // ... same as before
};

struct Inventory {
    struct Item *items; // A pointer to a block of Items on the heap
    int num_items;      // The number of items currently in the inventory
    int capacity;       // The number of items the allocated block can hold
};

This is a critical distinction. The Inventory struct no longer contains the items; it contains a handle to them. This allows us to change the size and location of the item data without changing the Inventory struct itself.

The following diagram shows the typical memory layout for this kind of structure. The Inventory struct itself can live on the stack (as a local variable in main, for example), while its items pointer points to a much larger block of memory allocated on the heap.

This diagram shows the different memory segments of a C program. Our `Inventory` struct might live on the stack, while the memory block for its `items`, allocated via `malloc` or `realloc`, resides on the heap.

This diagram shows the different memory segments of a C program. Our Inventory struct might live on the stack, while the memory block for its items, allocated via malloc or realloc, resides on the heap.


Building the Dynamic Inventory

Let's look at how to implement this new system. We'll start by analyzing a real-world example of a programmer facing this exact challenge.

C - Creating an array of strings dynamically (malloc)

This Stack Overflow post is a perfect case study. A student is trying to create a dynamic inventory for a text-based game and is running into common malloc pitfalls.

First, read the user's question under MY PROBLEM. You'll see their goal is identical to ours. Then, read the first part of the top answer, which analyzes their code and points out the errors, like the analysis here. This is a great refresher on what not to do.

The user's confusion is common. The correct approach, as hinted in the answers, involves using a dedicated struct to manage the pointer, count, and capacity, and using realloc to grow the allocated memory block.

1. Initialization and Cleanup

First, we need functions to create and destroy our inventory. An inventory_init function will set up the initial empty state, and a corresponding inventory_free function will release the heap memory we've used.

// inventory.h
#include <stdlib.h> // For NULL

// ... Item and Inventory struct definitions ...

void inventory_init(struct Inventory *inv);
void inventory_free(struct Inventory *inv);
// ... other function declarations ...

// inventory.c
#include "inventory.h"

void inventory_init(struct Inventory *inv) {
    inv->items = NULL; // Start with no allocated memory
    inv->num_items = 0;
    inv->capacity = 0;
}

void inventory_free(struct Inventory *inv) {
    free(inv->items); // Safe to call free(NULL)
    inventory_init(inv); // Reset to a clean state
}

Starting with items = NULL and capacity = 0 is a clean and common pattern. The C standard specifies that realloc(NULL, size) behaves identically to malloc(size), so our add_item function won't need a special case for the very first allocation.

2. Adding Items with the Doubling Strategy

Now for the core logic: adding an item. This function will embody the safe realloc pattern and the doubling strategy we discussed in the last lesson.

Dynamic Arrays in C

This video by Tsoding provides a fast-paced, practical demonstration of the core logic for a dynamic array.

Watch from the beginning to the implementation. Focus on the structure of his if check (count >= capacity), the handling of the initial zero-capacity case, and the use of realloc to grow the array. This is the exact algorithm we will implement.

Applying this logic to our Inventory struct, the inventory_add_item function will look like this:

// In inventory.c
#include <stdio.h>
#include <stdlib.h>

#define INITIAL_CAPACITY 8 // A reasonable starting size

void inventory_add_item(struct Inventory *inv, struct Item new_item) {
    // Step 1: Check if we are at capacity.
    if (inv->num_items >= inv->capacity) {
        // Step 2: If so, calculate new capacity using the doubling strategy.
        int new_capacity = (inv->capacity == 0) ? INITIAL_CAPACITY : inv->capacity * 2;
        
        // Step 3: Attempt to reallocate using the safe pattern.
        struct Item *temp = realloc(inv->items, new_capacity * sizeof(struct Item));
        if (temp == NULL) {
            fprintf(stderr, "Failed to expand inventory.\n");
            // Here you could free memory and exit, or just return.
            // For now, we'll just fail the add operation.
            return; 
        }

        // Step 4: If successful, update the inventory's pointer and capacity.
        inv->items = temp;
        inv->capacity = new_capacity;
        printf("Inventory resized to capacity %d\n", inv->capacity);
    }

    // Step 5: Add the new item and increment the count.
    inv->items[inv->num_items] = new_item;
    inv->num_items++;
}

This function is the heart of our dynamic system. It handles growth automatically, ensuring there's always room for a new item, while being efficient by minimizing the number of expensive realloc calls.

This diagram illustrates what happens during a resize. A new, larger memory block is found, the old data is copied, the old block is freed, and our `inv->items` pointer is updated to the new address.

This diagram illustrates what happens during a resize. A new, larger memory block is found, the old data is copied, the old block is freed, and our inv->items pointer is updated to the new address.


A Critical Detail: Freeing Complex Structs

Our current implementation works perfectly for Item structs that only contain simple data types (int, float, char[]). However, what if an Item itself contains a pointer to other dynamically allocated memory? For example:

struct Item {
    int id;
    char *name; // Dynamically allocated string
};

In this case, realloc performs a shallow copy. It copies the value of the name pointer, but not the string data it points to. This is usually what you want. The problem arises during cleanup. A simple free(inv->items) will free the array of struct Item, but it will not free the memory allocated for each individual name. This creates a memory leak.

Dynamically Allocate An Array Of Structs | C Programming Tutorial

The Portfolio Courses channel provides an excellent walkthrough of this exact scenario. First, watch how realloc is used with an array of structs.

Start by watching the segment on reallocating structs. Then, continue with the crucial explanation of freeing nested pointers. This demonstrates why you must loop through the array and free each dynamically allocated member before freeing the array itself.

To handle this correctly, our inventory_free function must become a "deep free" function. It must iterate through the inventory and free the memory for each item's members before freeing the container array.

void inventory_free(struct Inventory *inv) {
    // Step 1: Free the memory for each item's internal pointers.
    for (int i = 0; i < inv->num_items; i++) {
        free(inv->items[i].name); // Assuming 'name' was malloc'd
    }

    // Step 2: Free the array of items itself.
    free(inv->items);

    // Step 3: Reset the struct to a clean state.
    inventory_init(inv);
}

This two-step cleanup is essential for writing leak-free C programs with complex, nested data structures—a common requirement in game development.


Conclusion

You have now successfully refactored a static, array-based system into a robust, dynamic one. This is a major step forward in your C programming journey and directly applies to countless problems in game development, from managing inventories and quest logs to lists of enemies or particles.

Key Takeaways:

  • A dynamic array is managed with a struct containing a pointer, a count of elements, and the allocated capacity.
  • The inventory_add_item function is the core of the system, using the safe realloc pattern and the doubling strategy to grow the array efficiently.
  • A dynamic structure requires explicit memory management: inventory_init to set the initial state and inventory_free to release all allocated memory.
  • When structs themselves contain dynamically allocated data (like char*), you must implement a "deep free" by freeing the inner data before freeing the outer container array to prevent memory leaks.

In our next module, we will explore an entirely different approach to managing dynamic collections: linked lists. You'll learn how they differ from dynamic arrays in memory layout and performance, and when you might choose one over the other for different game systems.