Introduction to C: Arrays, Structure Initialization, and Increment Operators

In this lesson, we'll enhance our example code to make it more like a text adventure game. We'll create rooms that our player can explore, and add treasure for our player to collect.

Writing room.h

First we need a basic structure for each room the player will explore. We'll create a simple room.h as follows:

#ifndef _ROOM_H
#define _ROOM_H

struct room
{
    char* title;
    int gold;
};

#endif // _ROOM_H

Defining our rooms

To define our rooms, we'll create an array of room structures. First we will #include "room.h" within main.c, then we'll add the following code:

    struct room rooms[] = {
        {"Cave Entrance", 0},
        {"Dark Passageway", 15},
        {"Dragon's Lair", 30},
        {"Treasure Room", 50},
    };

First, notice that we have used the [] character when defining our rooms variable. This tells C to allocate memory for several struct rooms. Second, notice that (within curly braces - { and }) we have made a list of values that we would like placed into our struct rooms. Initializing our rooms in this way is called structure initialization in C.

You can also write struct room rooms[4] here. By leaving the number out, we allow C to make its own decision about how large of an array to allocate. If you were to write struct room rooms[3], that would casue a compiler error, because we have defined 4 rooms, but have only allocated enough memory for three.

Starting with the C99 standard, you could similarly write `{.title = "Cave Entrance"}. This allows you to initialize just a few elements of a large structure, and leave the remaining structure members set to zero.

Using the sizeof keyword to get our array length

Though we haven't specified the number of elements in our rooms array, we might want to know how many elements it contains. We can use the sizeof keyword to find that out, such as by declaring a variable as follows:

    int num_rooms = sizeof(rooms) / sizeof(struct room);

The

Verifying the sizeof numbers using gdb

Let's take a quick look at how to check our assumptions using gdb, the GNU debugger. By using gdb, we can set a breakpoint and run our program one line at a time.

$ gdb ./main

(gdb) b main.c:25
Breakpoint 1 at 0x12fc: file main.c, line 25.

(gdb) r
Starting program: /home/mpontillo/projects/intro-to-c/main 

Breakpoint 1, main (argc=1, argv=0x7fffffffdd78) at main.c:25
25      int num_rooms = sizeof(rooms) / sizeof(struct room);
(gdb) p num_rooms
$1 = 0

(gdb) n
26      int gold = 0;

(gdb) p num_rooms
$2 = 4

(gdb) p sizeof(rooms)
$3 = 64

(gdb) p sizeof(struct room)
$4 = 16

First, in order to run gdb, we pass it the path to the program we want to debug (in this case, ./main).

Next, we use the b command to set a breakpoint, in order to examine what happens when the code runs. I've chosen main.c:25, because line 25 in main.c is the line where num_rooms is initialized.

The r command can be used to run the program. (If your program requires additional arguments, you can supply them after the r command, such as r example.txt.)

When the program is run, the program returns to the (gdb) prompt after reaching line 25 in main.c. We can then use the p command to print values that we are interested in.

Notice that the first time we use the p command (p num_rooms), gdb reports a value of 0. This is because the line hasn't been run yet. gdb stops the program before the line executes. In order to execute the line and move on to the next line, we use the n command.

After executing the n command, we can use the p command to print the value of num_rooms again. This time, is's the value we expect: 4 (because we used structure initialization to define four rooms).

We can also use the sizeof keyword in GDB. As you can see, sizeof(rooms) is 64, and sizeof(struct room) is 16. Thus, our program correctly determines that 64 / 16 == 4, and num_rooms has the expected value.

Writing the go command

In the portion of our code where we've defined our commands, let's add another else if to handle the go command:

        } else if(strcasecmp("go", line) == 0) {
            current_room++;
            if(current_room >= num_rooms) {
                printf("Dungeon complete! You collected %d gold.\n", gold);
                playing = false;
            } else {
                printf(" ** %s **\n", rooms[current_room].title);
                printf("Collected %d gold.\n", rooms[current_room].gold);
                gold += rooms[current_room].gold;
            }

Notice the use of ++ in current_room++. This is the post-increment operator in C. It instructs the program to add one to the value of current_room after the statement executes.

We could have written ++current_room here, and it would be the equivalent. This is called the pre-increment operator. In more complex statements, using the pre-increment operator could alter the result of another calculation contained in the same statement.

The current_room variable indicates which room the player is currently inside. If the player proceeds beyond the last room we have defined, we will need to take a special action to stop them. Otherwise, our program would attempt to access memory beyond the end of our structure.

Layout of the rooms array

At this point, it may help to visualize what the rooms array looks like. As discussed in a previous lesson, array indexes in C begin with 0, and continues as follows:

Index 0 1 2 3
Title Cave Entrance Dark Passageway Dragon's Lair Treasure Room
Gold 0 15 30 50

That is, while the size of the array is 4, accessing rooms[4] will access memory beyond what has been allocated for our rooms array, and is thus undefined behavior. Our program could crash if this occurs, or it could interpret the surrounding memory as if it was a struct room, thus confusing our users with a bug and printing "garbage" to the screen.

That being the case, before we access the rooms array, we want to be sure the current_room is between zero and three. We know current_room can not be less than zero, so if we write an if statement as follows, we can check if we're still in a valid room:

    if(current_room >= num_rooms) { /* ... */ }

That is, if we increment current_room to 4 or more, the player has moved through all the possible rooms. If that happens, we'll set playing = false so that the game will be over.

Using the += operator

If the player has NOT been through all the rooms, we'd like to print the title of the room, and collect whatever gold happens to be in it. So we'll write an else statement to complete the aforementioned if/else as follows:

            if(current_room >= num_rooms) {
                printf("Dungeon complete! You collected %d gold.\n", gold);
                playing = false;
            } else {
                printf(" ** %s **\n", rooms[current_room].title);
                printf("Collected %d gold.\n", rooms[current_room].gold);
                gold += rooms[current_room].gold;
            }

The += operator will take the current value of gold, and add to it the value of the statement on the right side (also called the rvalue; in this case rooms[current_room].gold). Then it will take the result and store it back in the gold variable.

More generally, writing varable += value is equivalent to writing variable = variable + value.

Running our Program

Now we're ready to test our program:

$ ./main
What is your name?
> Mike
Welcome to the tutorial dungeon! There are 4 rooms to explore.
Player name: Mike
Level: 1
Health points: 10/10
> go
 ** Dark Passageway **
Collected 15 gold.
> go
 ** Dragon's Lair **
Collected 30 gold.
> go
 ** Treasure Room **
Collected 50 gold.
> go
Dungeon complete! You collected 95 gold.

You may have noticed one subtle bug: since current_room starts at zero and is immediately incremented, ** Cave Entrance ** is never printed out.

Conclusion

In this lesson, you've learned how to initialize an array of structures, increment a value, and use gdb to run your program step-by-step.

Be sure to check back later for the next lesson, or send feedback about what you'd like to see next!