Remember arrays? Hopefully you do, but here’s a very quick refresher:
Arrays represent a collection of items, usually with the same datatype. Here’s our array of grades example from before:
float grades[5];
[0] = 92.5;
grades[1] = 52.4;
grades[2] = 75.0;
grades[3] = 82.0;
grades[4] = 88.9; grades
grades[0] grades[1] grades[2] grades[3] grades[4]
+------------+------------+------------+------------+------------+
| 92.5 | 52.4 | 75.0 | 82.0 | 88.9 |
+------------+------------+------------+------------+------------+
This specific array, of type float
, and a size of 5
represents a one-dimensional list of grades. A single row, if
you will. However, there are some cases where we might need more than
one dimension. Let’s say you wanted to represent a grid of some kind,
like in tic-tac-toe. We could just create another
one-dimensional array of size 9, but we would have to always keep track
of what row and column we’re on, since a one-dimensional array only
allows us to access the element with just a single index.
However, what if I told you that C allows you to create multi-dimensional arrays? One of the main advantages of this, for instance, is that we can access array elements using their “row” and “column”.
Take a look at this example:
char tic_tac_toe[3][3]; // create a 3x3 two-dimensional array for tic-tac-toe purposes
This creates an empty two-dimensional array, with 3 rows and 3 columns. Labeled is a representation of the grid, with the row indexes and column indexes labeled:
column index
0 1 2
+-----+-----+-----+
0 | | | |
+-----+-----+-----+
row 1 | | | |
index +-----+-----+-----+
2 | | | |
+-----+-----+-----+
Now first, we want to make sure we can see the grid, so we’re going to populate our array with underscores so we can view each “cell”. Don’t worry, we’ll go over what the code means later, but for right now, just know that we’re looping through each cell and placing an underscore in it.
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
/* initalize the char at the row, col of tic_tac_toe
to an underscore for better readability */
[i][j] = '_';
tic_tac_toe}
}
Of course, you could have used another character, or maybe you’re content with just using a space, but remember to initialize your values, else you could accidentally end up reading garbage values!
Now, let’s say that we want to put an X
in the bottom
left cell:
Remember, array indexes start at 0. So although we have three rows and three columns (a 3x3 grid), we must remember to start each index at 0.
[2][0] = 'X' // set an 'X' in the third row, first column of the 2d array tic_tac_toe
Now, our grid looks like this:
column index
0 1 2
+-----+-----+-----+
0 | | | |
+-----+-----+-----+
row 1 | | | |
index +-----+-----+-----+
2 | X | | |
+-----+-----+-----+
As you can see, we’ve just placed an ‘X’ onto the bottom left of our 2D array!
To print information from a 2D array in C, you can follow the same
concept you might use when printing a 1D array. For instance, you
probably understand that this will print the array, arr
, in
a linear fashion, separated by spaces:
#include <stdio.h>
int main(void) {
int arr[] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
("%d ", arr[i]);
printf}
("\n");
printfreturn 0;
}
Now, let’s think back to our tic-tac-toe example. If we must keep track of both row and column index, how could we construct our new for loop?
Trick question! We would need multiple for loops! Specifically, we would need to nest two for loops together. Take a look at the following code:
#include <stdio.h>
int main(void) {
char tic_tac_toe[3][3];
[2][0] = 'X';
tic_tac_toe
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
/* initalize the char at the row, col of tic_tac_toe
to an underscore for better readability */
[i][j] = '_';
tic_tac_toe}
}
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
("%c ", tic_tac_toe[i][j]);
printf}
("\n");
printf}
return 0;
}
To explain what’s going on, let’s walk through the important bits:
char tic_tac_toe[3][3];
[2][0] = 'X'; tic_tac_toe
Here, we’re just creating our tic-tac-toe 2D char
array,
with a size of 3x3. Then, we’re setting the character at the third row,
first column to ‘X’. Remember, arrays in C start at index 0!
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
This may look a little strange, but all that is essentially going on in these two lines is that we’re setting up the first, “outer” loop so that we start at i = 0, all the way to i = 2, then increment. Then, for each iteration of that outer loop, we are also looping through another loop located inside that first loop. So, if we track the variables:
i-index j-index (i, j)
________________________
i = 0 j = 0 (0, 0)
j = 1 (0, 1)
j = 2 (0, 2)
<- finished inner loop, now we i++
i = 1 j = 0 (1, 0)
j = 1 (1, 1)
j = 2 (1, 2)
<- again, finish inner loop, now we i++
i = 2 j = 0 (2, 0)
j = 1 (2, 1)
j = 2 (2, 2)
<- both i and j have reached their terminating conditions
You’ll see that for every iteration of i
, we finish up
the inner loop first (e.g. by incrementing until we reach the end
condition), then finally going back to the outer loop and incrementing
its counter.
Now, you may realize something: if we look at (i, j) when tracking
the variables, the indexes line up with the positions on our tic-tac-toe
grid, at least in terms of accessing them, like we did with
tic_tac_toe[2][0] = 'X'
. For instance, (0, 0) represents
the top-left, (0, 1) represents the cell right next to that, (0, 2) then
is the top-right, etc.
We now have a way to generate the indexes of all of the possible cells in our 2D array for tic-tac-toe! So, now, we can look at the next line:
("%c ", tic_tac_toe[i][j]); printf
Here, we print a single character (with a space after it), by
accessing what’s in our 2D array. Just like what we did before, when
trying to assign ‘X’ to tic_tac_toe[2][0]
, in this line of
code, we access the character in tic_tac_toe[i][j]
. For
instance, if we randomly stopped the program from running when i = 2 and
j = 0, we would see that the character at tic_tac_toe[2][0]
is, in fact, X
.
Finally, when we run our program, we get the following output:
_ _ _
_ _ _
X _ _
Wow! A tic-tac-toe grid. If we really wanted to, we can even start to play a full game of tic-tac-toe by assigning values:
[2][0] = 'X';
tic_tac_toe[0][0] = 'O';
tic_tac_toe[1][0] = 'X';
tic_tac_toe[0][1] = 'O';
tic_tac_toe[2][1] = 'X';
tic_tac_toe[2][2] = 'O';
tic_tac_toe[1][2] = 'X';
tic_tac_toe[1][1] = 'O';
tic_tac_toe[0][2] = 'X'; tic_tac_toe
O O X
X O X
X X O
Of course, you really don’t want to manually have to edit your source code and recompile your program every time you make a move. Perhaps you can try extending your program to accept input from the user, and make this a full, playable tic-tac-toe game!
Assuming you used _
as a separator, and your final code
may look something like this:
#include <stdio.h>
int main(void) {
char tic_tac_toe[3][3];
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
[i][j] = '_';
tic_tac_toe}
}
// find a better way to do this!!
// I can't just keep recompiling every time I make a move!
[2][0] = 'X';
tic_tac_toe[0][0] = 'O';
tic_tac_toe[1][0] = 'X';
tic_tac_toe[0][1] = 'O';
tic_tac_toe[2][1] = 'X';
tic_tac_toe[2][2] = 'O';
tic_tac_toe[1][2] = 'X';
tic_tac_toe[1][1] = 'O';
tic_tac_toe[0][2] = 'X';
tic_tac_toe
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
("%c ", tic_tac_toe[i][j]);
printf}
("\n");
printf}
return 0;
}
If you haven’t learned about dynamically allocating memory yet, then this section may not make much sense. Consider viewing the section on Pointers and Dynamic Memory Allocation first.
However, if you have, then this section will detail some of the thought behind dynamically allocating multi-dimensional arrays.
Note: There are a multitude of ways to implement 2D arrays in C. This is simply one of them, which may not be the most efficient.
Before, we saw how to create a 2D array in the stack space, like the following example below:
char tic_tac_toe[3][3];
Of course, this only creates a 2D array in the stack space.
Let’s try to create our tic-tac-toe array in the heap with
malloc
(assuming you already have
#include <stdlib.h>
).
char **tic_tac_toe = malloc(sizeof(char *) * 3);
Let’s draw out what’s currently happening:
We now have a double pointer, **tic_tac_toe
, which holds
an address that points to our array of char
pointers. We aren’t done yet, however. So far, we’ve only laid out the
foundation of what will become our 2D array.
If you’ve been paying close attention, you might realize that each
“cell” in our **tic_tac_toe
array of char *
could be made into arrays. And that’s exactly what we’ll do: for each
char *
cell in **tic_tac_toe
, we can
initialize (using malloc
again) a new array of
char
, thus finally finishing the initialization of our 2D
array.
How would we do that? Well, you could individually access each
char *
cell, malloc
a new char
array for each, and then eventually place a value inside each cell. It’s
a bit tedious, but I’ll explain it here:
// create the "double pointer" that points to an array of (empty) arrays
char **tic_tac_toe = malloc(sizeof(char *) * 3);
// individually access each cell of tic_tac_toe (which are of type (char *))
// so that we can then allocate space in each cell for char arrays
[0] = malloc(sizeof(char) * 3);
tic_tac_toe[1] = malloc(sizeof(char) * 3);
tic_tac_toe[2] = malloc(sizeof(char) * 3);
tic_tac_toe// finally, we can access each element like a 2d array:
[0][0] = 'X'; tic_tac_toe
Of course, you may know now that you could also use a loop to
automate our malloc
calls for each cell in
**tic_tac_toe
, so for instance, you might do something like
this:
char **tic_tac_toe = malloc(sizeof(char *) * 3);
for (int i = 0; i < 3; i++) {
[i] = malloc(sizeof(char) * 3);
tic_tac_toe}
Since we used malloc
, we need to properly free our
memory, or else we could get spooky memory leaks! Using the previous
example, freeing is simple:
char *
in
**tic_tac_toe
. Here, we’ll use a loop to make things less
tedious:for (int i = 0; i < 3; i++) {
(tic_tac_toe[i]);
free}
tic_tac_toe
itself.(tic_tac_toe); free
We now have freed all the necessary memory we
malloc
’d!