🎯 Pointers & Memory
Multidimensional arrays: grids in a flat world
▶ Open the interactive lesson — free, no signupChess boards, Minesweeper fields, spreadsheets, every photo on your screen — all grids. But memory, as you've seen since Part 0, is one straight line of bytes, so C has to fold each grid into that line. Learn the fold and you'll be able to hand grids to functions without baffling compiler errors — and you'll see why looping over a big grid in the wrong order can make the exact same code several times slower.
Memory is one long line of bytes — there is no "up" or "down" in RAM. So how does C store a grid like int m[2][3]? By a beautifully simple trick: a 2-D array is an array of arrays. m is 2 elements long, and each element is itself an int[3] row.
Declaring and looping
#include <stdio.h>
int main(void) {
int m[2][3] = {
{1, 2, 3}, /* row 0 */
{4, 5, 6}, /* row 1 */
};
for (int r = 0; r < 2; r++) {
for (int c = 0; c < 3; c++)
printf("%d ", m[r][c]);
printf("\n");
}
printf("sizeof m = %zu\n", sizeof m); /* whole grid */
printf("sizeof m[0] = %zu\n", sizeof m[0]); /* one row */
return 0;
}$ gcc grid.c -o grid && ./grid 1 2 3 4 5 6 sizeof m = 24 sizeof m[0] = 12
sizeof m[0] is 12 — one whole row. That confirms the "array of arrays" story: m[1] is a real int[3], and m[1][2] indexes into it.
🧠 Checkpoint: What exactly is m[1] for int m[2][3]?
- An int
- A pointer stored in memory next to m[0]
- The second row — a real
int[3]array - A syntax error without a second index
Show answer
The second row — a real int[3] array — A 2-D array is an array of arrays: m[1] is the second row, an int[3] living 12 bytes after the start. (In expressions it happily decays to an int* like any array.)
Row-major: the grid, flattened
The rows are laid end-to-end in one contiguous block — row 0 first, then row 1. This is called row-major order:
▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
So the address math for m[r][c] is: base + (r * COLS + c) * sizeof(int). Skip r full rows, then c elements into the row. We can prove the flatness by walking the whole grid with a single pointer:
#include <stdio.h>
int main(void) {
int m[2][3] = {{1, 2, 3}, {4, 5, 6}};
int *flat = &m[0][0]; /* first int of the block */
/* m[r][c] lives (r*3 + c) elements from the start: */
printf("m[1][2] = %d\n", m[1][2]);
printf("flat[1*3+2] = %d\n", flat[1*3 + 2]);
printf("&m[0][0] = %p\n", (void *)&m[0][0]);
printf("&m[1][0] = %p\n", (void *)&m[1][0]); /* +12 bytes */
return 0;
}$ gcc flat.c -o flat && ./flat m[1][2] = 6 flat[1*3+2] = 6 &m[0][0] = 0x7ffcd58e91b0 &m[1][0] = 0x7ffcd58e91bc # 0x1bc - 0x1b0 = 0xc = 12 bytes = one full row
Performance bonus: because rows are contiguous, looping row by row (the inner loop over columns) touches memory sequentially and keeps the CPU cache happy. Loop column-first over a big matrix and you can easily go several times slower — same math, worse order.
🧠 Checkpoint: For int m[4][5] (4-byte ints), what is the byte offset of m[2][3] from the start?
- 23
- 32
- 52
- 92
Show answer
52 — Offset = (r × COLS + c) × sizeof(int) = (2×5 + 3) × 4 = 13 × 4 = 52 bytes. Skip two full rows (40 bytes), then three ints (12 more).
Passing 2-D arrays to functions
When a 2-D array decays, it becomes a pointer to its first element — and the first element is a row. So int m[2][3] decays to int (*)[3]: "pointer to array of 3 ints". That's why the parameter must spell out the inner size:
#include <stdio.h>
/* the inner size (3) is REQUIRED — it sets the row stride */
int sum(int rows, int m[][3]) { /* same as int (*m)[3] */
int s = 0;
for (int r = 0; r < rows; r++)
for (int c = 0; c < 3; c++)
s += m[r][c];
return s;
}
int main(void) {
int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
printf("sum = %d\n", sum(2, grid));
return 0;
}$ gcc pass2d.c -o pass2d && ./pass2d sum = 21
Why is the 3 mandatory? Look at the address math above: computing m[r][c] needs COLS. Without the inner dimension the compiler literally cannot find row 1. (The outer size is still decorative, as always.)
🧠 Checkpoint: Why must a 2-D array parameter be written int m[][4] — what is the 4 for?
- Pure documentation
- The compiler needs the row width to compute the address of m[i][j]
- It makes the compiler bounds-check columns
- It limits callers to exactly 4 rows
Show answer
The compiler needs the row width to compute the address of m[i][j] — m[i][j] compiles to base + (i×4 + j)×sizeof(int). Drop the 4 and the stride is unknown — the compiler rejects it. The OUTER dimension can be omitted, as usual.
The impostor: arrays of pointers
char *menu[3] looks 2-D when you write menu[i][j], but it's a completely different animal: an array of 3 pointers, each aiming at a separately-stored string, possibly of different lengths ("jagged"). Two dereferences instead of one address calculation:
▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
Both support x[i][j] syntax — which is exactly why people mix them up. True 2-D: one block, address math. Array of pointers: a table of arrows. You'll build the jagged kind yourself once you can malloc — which is the very next lesson.
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
▶ Practice this lesson interactively (with live gcc)