Skip to main content

Welcome to a Grasp lesson

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

Lesson 57

Saving Inventory Data to a Text File

Recommended resources

Introduction

In our last lesson, we established a robust framework for handling file I/O, focusing on how to detect, diagnose, and manage errors using tools like errno and perror. This ensures our applications don't fail silently. Now, we can confidently move on to a practical and essential task for your game project: saving the game's state.

This lesson bridges the gap between the dynamic data structures we've built—specifically, the linked list for your inventory—and the persistent storage we've learned to manage. We will address the crucial question: how do you convert a complex, memory-based structure like a linked list into a format that can be written to a file and stored permanently? You will learn to traverse your inventory list and serialize its contents into a well-structured text file, creating the foundation for your game's save system.


The Challenge: Pointers in Memory vs. Data on Disk

Let's begin by considering the structure of our linked list. Each node contains data (the item) and a pointer to the next node. This chain of pointers, starting from the head, is what defines the list in memory.

A singly linked list consists of nodes, each holding data and an address (pointer) to the next node. The `head` pointer marks the beginning of the list.

A common first thought might be to simply write the entire Node struct to a file for each item in the list. However, this approach has a fatal flaw. The "address" field in each node is a memory address, a location valid only for the current execution of your program. If you were to save these addresses and try to read them back later, they would be meaningless at best, and at worst, point to invalid or protected memory, causing your program to crash.

The video below explains this fundamental problem very clearly.

Reading and writing a linked list to a file in C

In this clip from the CodeVault channel, the presenter explains why you cannot directly serialize a data structure containing pointers.

Watch the first minute of the video, from the beginning to the end of the introduction. Pay close attention to the reasoning about why a deserialized pointer would not be a valid address.

The solution, as the video introduces, is serialization. This is the process of converting the in-memory data structure into a format suitable for storage. For our linked list, this means we must traverse the list and write out only the actual data—the item's ID, name, quantity, etc.—while discarding the pointers that link them together in memory.


Designing a Parsable File Format

Once we commit to saving only the data, we face a new design choice: how should we format that data in the text file? Simply writing all the data back-to-back would be a mistake. For instance, if you have an item with ID 12 and quantity 5, writing them with fprintf(fp, "%d%d", id, quantity) would produce 125 in the file. When you try to read this back, how can the program know where the ID ends and the quantity begins?

This is a classic problem in data storage, and the Stack Overflow discussion below highlights this exact pitfall.

Saving from a linked list to a file and loading it back

This Stack Overflow thread discusses a user's struggle with saving and loading a linked list, stemming from an improper file format.

First, look at the original poster's save function under the section the save function. Notice the fprintf format string, which just concatenates all the data fields. Then, read the first answer's explanation about why this is problematic in the paragraph starting with "In the example code...".

To solve this, we need a clear and consistent format. There are two primary strategies for text files:

  1. Delimited Format: Separate each data field with a special character (a delimiter), such as a comma (,), semicolon (;), or pipe (|). Each record (a full node's data) is typically separated by a newline character (\n). This is the basis for formats like CSV (Comma-Separated Values). It's human-readable and flexible.

  2. Fixed-Width Format: Pad each data field with spaces so it occupies a specific number of characters in the file. For example, the ID always takes 10 characters, the name always takes 50, etc. This can be simpler to parse but is less flexible and can waste space if data is shorter than the allotted width.

For our game's inventory, a delimited format is an excellent choice. It's easy to read for debugging and adapts well to changes, such as adding new item properties in the future. Let's choose a pipe character (|) as our delimiter, since item names themselves might contain commas or spaces. Each line in our file will represent one item.

Format per line: id|name|quantity|value\n


Implementing the Save Function

Now we can combine these ideas into a C function that saves our inventory. The logic is straightforward:

  1. Open the target file in write mode ("w"), performing robust error checking as we learned in the previous lesson.
  2. Create a temporary pointer, current, initialized to the list's head.
  3. Loop as long as current is not NULL.
  4. Inside the loop, use fprintf to write the data from the current node to the file, using our chosen delimited format.
  5. Move to the next node: current = current->next;.
  6. After the loop finishes, close the file.

The following video demonstrates this exact process, using fprintf to write the data from each node in a loop.

Reading and writing a linked list to a file in C

This segment from CodeVault demonstrates the implementation of a serialize function that writes a linked list's data to a file.

Watch from the implementation of serialize. Although the video uses a simple list of integers, the pattern of opening a file, looping through the list, and using fprintf is directly applicable. You can stop watching after the fclose call.

Let's adapt this pattern to our game's Item inventory. Assuming we have the Item and Node structs from our previous work, the save function would look like this:




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

// Assume these structs are defined in a header like "inventory.h"
typedef struct {
    int id;
    char name[50];
    int quantity;
    double value;
} Item;

typedef struct Node {
    Item data;
    struct Node *next;
} Node;

/**
 * @brief Saves the contents of a linked list inventory to a text file.
 * 
 * @param head A pointer to the first node of the linked list.
 * @param filename The path to the file where the inventory will be saved.
 * @return 0 on success, -1 on failure.
 */
int save_inventory_to_file(Node* head, const char* filename) {
    FILE* fp = fopen(filename, "w");
    if (fp == NULL) {
        perror("Failed to open save file for writing");
        return -1; // Indicate failure
    }

    printf("Saving inventory to %s...\n", filename);

    Node* current = head;
    while (current != NULL) {
        // Write the data for the current item in our pipe-delimited format
        fprintf(fp, "%d|%s|%d|%.2f\n", 
                current->data.id,
                current->data.name,
                current->data.quantity,
                current->data.value);
        
        // Move to the next node
        current = current->next;
    }

    fclose(fp);
    printf("Inventory saved successfully.\n");
    return 0; // Indicate success
}

If you run this function on an inventory list, it will produce a savegame.txt file that might look something like this:

101|Health Potion|5|25.00
24|Iron Sword|1|150.00
3|Lockpick|10|5.50

This file is both human-readable and, more importantly, structured in a way that allows a program to parse it and reconstruct the linked list—a task we will tackle in our next lesson.


Conclusion

In this lesson, you've mastered the critical technique of serializing a dynamic data structure to a persistent file. By converting your in-memory linked list into a well-defined text format, you have created a durable representation of your game's inventory.

Key Takeaways:

  • Memory addresses (pointers) are transient and cannot be saved to a file for later use. You must serialize the data itself.
  • Serialization involves traversing the data structure and writing its contents to a file in a consistent, parsable format.
  • A well-designed file format is crucial for successful deserialization. Delimited formats (like CSV or our pipe-separated version) are a flexible and common choice for text-based storage.
  • The implementation combines list traversal (while (current != NULL)) with file writing (fprintf) in a loop, applying the robust file handling practices from our previous lesson.

You have now successfully written your game data to disk. The next logical step is to read it back. In our next lesson, we will build the other half of this system: a function that loads data from your save file, dynamically allocates memory for each item, and reconstructs the linked list from scratch.