Introduction to C: Structures

Structures in C are user-defined data types that make it easier to organize related data. The struct keyword is used to define a structure.

For this lesson, we'll change our program so that the name the user enters becomes part of a structure. We'll start evolving our example program into a game, and the text the user enters will become the player's name.

Creating player.h

First we'll define player.h, including the usual #ifndef / #define / #endif pattern (as discussed in the previous lesson.

Then we'll create our first struct:

struct player
{
    char* name;
    int level;
    int health;
    int max_health;
};

This defines struct player, the data type for our player structure. It includes a field for the player's name, their level, the amount of health points, and their maximum health (max_health).

Working with Structures

When creating a data structure (in this case, our player structure), it's helpful to organize your code so that operations that use the structure are easy to find. Thus, we'll create player.c, which will contain opreations to create, destroy, and print the status of the player.

Creating a new struct player

High level languages often provide a constructor and destructor to create and destroy objects. Since C doesn't do this for us, we'll start by creating functions to allocate and free the memory for our structure.

We must start with an #include "player.h" directive, in order to get the definition of struct player. After that, let's write a couple of functions

Writing player.c

The player_new() function
struct player* player_new(char* name) {
    struct player* player = NULL;

    player = calloc(1, sizeof(struct player));
    if(player == NULL) {
        perror(__func__);
        exit(1);
    }
    player->name = name;
    player->level = 1;
    player->health = 10;
    player->max_health = 10;

    return player;
}

The player_new() function will return a pointer to a (newly created) struct player.

We'll use the calloc() function (which requires us to #include <stdlib.h>) to allocate memory for our structure. Note that we could have used malloc() here, but I personally prefer to use calloc(), because (unlike malloc()) it initializes the allocated memory to all zeroes.

The calloc() function takes two parameters: the first is the number of structures to allocate, and the second is the size of the structure. By passing in a value other than 1 for the first parameter, we could have created an array of struct player allocations in a single call.

In C, bugs in your program can arise when using malloc() due to the fact that it does not initialize the memory at the same time as it allocates it. This leads to the appearance of "random garbage" (data from previous allocations that were subsequently freed) appearing in your structure. in your data by default, unless you separately clear it.

Standard memory allocation fuctions in C will return NULL if no memory has been allocated, so we check the result of calloc() before making use of the newly-allocated structure.

Using a Pointer to a Structure

The -> operator dereferences a pointer to a structure, and accesses one of its fields. In this case, we're using it to set the value of each field. (We'll also use it later to read its fields.)

When working with a structure that is not allocated using a pointer, you can use a ., such as player.name.

The player_free() function

Because our player structure includes a char* name (a pointer to a separately-allocated string containing the player name), when we free() our struct player* we must free up this string separately. Thus, we'll write a player_free() function as follows:

void player_free(struct player* player) {
    free(player->name);
    free(player);
}

You may be wondering what would happen if player_free() is called, but the player->name hasn't been set. This won't be a problem, because the free() function does not perform any operation if it is called with a NULL value.

It's good practice to set a pointer to NULL immediately after freeing it, to prevent a common problem called a double-free. However, in this case we are freeing the entire structure, so we'll skip that step.

A double-free can be a problem because subsequent malloc() or calloc() calls could re-use previously used memory, resulting in memory that is still in-use elsewhere being prematurely freed.

The player_print() function

Next, it would be helpful to print our struct player out to the console. Let's write player_print() as follows:

void player_print(struct player* player) {
    printf("Player name: %s\n", player->name);
    printf("Level: %d\n", player->level);
    printf("Health points: %d/%d\n", player->health, player->max_health);
}

This code uses the %d format string, which is suitable for printing a decimal integer (or int in C).

Putting it all together in main.c

Now that we've got our player.h and player.c, we can use our new functions in main.c. Let's add #include "player.h" to the top of main.c, and change our main() function as follows:

int main(int argc, char* argv[])
{
    char* line;
    struct player* player = NULL;

    printf("What is your name?\n");
    printf("> ");
    line = input_getline();
    player = player_new(line);
    player_print(player);
    player_free(player);

    return 0;
}

Notice that we are no longer calling free() on the line variable; that is because the player_free() function now handles that for us. (Ideally, we should write a comment in player.h to describe this behavior, or otherwise document it, so that whoever calls our player_*() functions knows what to expect regarding memory management.)

Running the program

$ make
cc -Wall -Werror -std=c11 -g -c player.c -o player.o
cc -Wall -Werror -std=c11 -g -c main.c -o main.o
cc -Wall -Werror -std=c11 -g -c input.c -o input.o
cc -o main *.o

$ ./main
What is your name?
> Mike 
Player name: Mike
Level: 1
Health points: 10/10

Conclusion

By now, you've learned how to create a structure definition, create functions to work with a structure, and use the -> operator to dereference a pointer to a structure and access one of its fields. You've also learned how to use calloc() to allocate and zero out memory.

In the next lesson, we'll cover the while loop, the boolean (bool) type, and how to compare strings.