Arrays & Strings · beginner · ~10 min

Multi-dimensional arrays

- Declare, initialize, and read 2D (and briefly 3D) arrays in C using `type name[rows][cols]`. - Explain **row-major order** and compute the flat memory offset of any `grid[r][c]`. - Iterate over a 2D array correctly with nested loops, keeping row and column indices straight. - Pass a 2D array to a function, knowing why the **inner (column) dimension** must be fixed. - Avoid out-of-bounds access and other common 2D-array bugs. - Understand when a real matrix is stored as a flat 1D array and how to index it manually (`r * cols + c`).

Overview

A multi-dimensional array lets you store data that is naturally shaped like a grid or table: a chessboard, a spreadsheet, the pixels of an image, a distance table between cities, or a small game map. Instead of juggling many separate variables, you keep everything in one organized structure and reach any cell with two (or more) indices.

This lesson builds directly on Arrays. A one-dimensional array gives you a row of values you index with one number, like scores[3]. A two-dimensional array is the next step: an array of arrays, where the first index picks a row and the second index picks a column within that row. If you are comfortable with int scores[10], you already know most of what you need — a 2D array just adds a second pair of brackets.

The most important idea here is not the syntax but the memory layout. C does not actually store a grid as a 2D picture; it stores all the values in one continuous, flat block of memory, laid out one full row at a time. This is called row-major order. Once you can picture that flat layout, everything else — nested loops, passing arrays to functions, treating a matrix as a flat 1D array — follows naturally.

Key terms used throughout: a row is one horizontal line of the grid (the first index); a column is one vertical position within a row (the second index); the dimensions are the sizes [rows][cols]; and row-major order is the storage rule that keeps each row's elements next to each other in memory.

Why it matters

Grids and tables are everywhere in real software, and 2D arrays are the most direct way to represent them in C:

  • Images are 2D arrays of pixels; image filters loop over rows and columns.
  • Games use 2D arrays for boards and maps (tic-tac-toe, chess, tile maps, Minesweeper).
  • Matrices in graphics, physics, and machine learning are 2D arrays of numbers.
  • Tables of data — temperatures by city and day, a seating chart, a spreadsheet region — map cleanly onto rows and columns.

Understanding row-major layout also makes your code faster and safer. Looping in the natural row-then-column order touches memory sequentially, which the CPU cache loves. And knowing exactly how many bytes a 2D array occupies, and where each cell lives, is what keeps you from reading or writing past the end — a leading cause of crashes and security bugs in C.

Core concepts

1. A 2D array is an array of arrays

Definition. int grid[3][4]; declares an array of 3 rows, where each row is itself an array of 4 int values — 12 ints total.

How to read it. The dimensions read left-to-right as outer-to-inner. grid has 3 elements; each of those elements is an int[4]. So grid[0] is the whole first row (a 4-int array), and grid[0][2] is the third element of that row.

When to use it. Use a fixed 2D array when the shape is known and modest in size: a board, a small lookup table, a fixed-size image tile. When the size is large or only known at runtime, you will usually allocate memory dynamically (a later topic) or use a flat 1D array with manual indexing (see concept 4).

Pitfall. Beginners often swap the order, thinking grid[3][4] means "columns 3, rows 4." In C the first index is always the row (the outer array).

Knowledge check: For double table[5][8];, how many double values are stored in total, and what is the type of table[0]?

2. Row-major order (the memory layout)

Definition. C stores all elements of a 2D array in one continuous block of memory, laid out row by row: the entire first row, then the entire second row, and so on. This is row-major order.

How it works internally. For int grid[3][4], the 12 ints sit back-to-back. The element grid[r][c] is found at flat position r * 4 + c (where 4 is the column count). In bytes, its address is base_address + (r * 4 + c) * sizeof(int).

int grid[3][4]   ->  one flat block of 12 ints (row-major)

index:   0    1    2    3 | 4    5    6    7 | 8    9   10   11
cell: [0,0][0,1][0,2][0,3]|[1,0][1,1][1,2][1,3]|[2,0][2,1][2,2][2,3]
        \------ row 0 -----/\------ row 1 -----/\------ row 2 -----/

flat offset of grid[r][c] = r * COLS + c   (here COLS = 4)
grid[2][1] -> 2*4 + 1 = offset 9

When it matters. Row-major order explains why the column count must be known to find any element, why looping row-then-column is cache-friendly, and how a flat 1D array can stand in for a matrix.

Pitfall. Assuming the layout is column-major (column by column). C is always row-major. Some other languages (Fortran, MATLAB) are column-major, so ported math code can index "transposed" if you forget this.

Knowledge check (predict the offset): In int grid[3][4], what is the flat offset of grid[1][3]? What about grid[2][0]?

3. Nested loops to traverse a 2D array

Definition. You visit every cell with two loops: an outer loop over rows and an inner loop over columns.

How it works. The outer loop fixes a row r; the inner loop walks every column c in that row before the outer loop advances. This matches row-major order, so memory is read in sequence.

for r in 0..rows-1:        outer (rows)
    for c in 0..cols-1:    inner (columns)
        use grid[r][c]

visit order for 3x4:
(0,0)(0,1)(0,2)(0,3) -> (1,0)(1,1)(1,2)(1,3) -> (2,0)(2,1)(2,2)(2,3)

When to use it. Any time you fill, print, search, or transform every cell. To process columns as the outer concern (e.g., sum each column), you can swap the loop order, but the row-then-column order is usually the most cache-friendly default.

Pitfall. Using the wrong bound for a loop — e.g., comparing the row index against the column count. Keep r < ROWS and c < COLS aligned with the right dimension.

Knowledge check (find the bug): A learner writes for (int r = 0; r < COLS; r++) for (int c = 0; c < ROWS; c++) grid[r][c] = 0; for int grid[ROWS][COLS] where ROWS != COLS. What goes wrong?

4. The inner dimension must be known (and flat 1D matrices)

Definition. When you declare a 2D array or write a function that takes one, the inner dimension (number of columns) must be a compile-time constant; only the outermost dimension may be left empty. The exception is a C99 variable-length array (VLA), which lets the size be a runtime value.

Why. To compute the address of grid[r][c], the compiler needs COLS for the r * COLS + c formula. The row count is not needed for the math, so it can be omitted; the column count cannot.

void f(int g[][4], int rows);   // OK: inner size 4 is known
void f(int g[][],  int rows);   // ERROR: inner size missing

Many real programs avoid the fixed-column rule by using a FLAT array:
  int *m;                       // length rows*cols
  m[r * cols + c]               // manual row-major indexing

When to use a flat array. When the dimensions are decided at runtime, or when you want one simple pointer to pass around. You do the r * cols + c math yourself — exactly the offset C computes for you in grid[r][c].

Pitfall. Forgetting to multiply by cols (writing m[r + c]) silently reads the wrong cell. There is no compiler error — just wrong results.

Knowledge check (explain in your own words): Why can the compiler accept int g[][4] as a function parameter but reject int g[][]?

Syntax notes

// Declaration: type name[ROWS][COLS];
int grid[3][4];                 // 3 rows, 4 columns, uninitialized

// Declaration + initialization (nested braces show rows)
int g[2][3] = { {1, 2, 3},      // row 0
                {4, 5, 6} };    // row 1

int z[2][3] = { 0 };            // every element set to 0

// Access: name[row][column]
g[1][2] = 99;                   // row 1, column 2 -> the value 6 becomes 99
int x = g[0][0];                // reads 1

// One whole row is itself a 1D array
int *row1 = g[1];               // row1 points to {4,5,99}

// Function parameter: inner (column) size is REQUIRED, rows optional
void print_grid(int g[][3], int rows);   // 3 = column count
// equivalently: void print_grid(int (*g)[3], int rows);

Notes:

  • The braces in the initializer are documentation for the rows; the values are still stored flat in row-major order.
  • g[i] (one index) gives you a whole row as an int[COLS], which decays to a pointer in most expressions.
  • In a function parameter, int g[][3] and int (*g)[3] mean the same thing: a pointer to an array of 3 ints (a row).

Lesson

What a 2D array really is

In C, a 2D array is an array of arrays.

For example:

int grid[3][4];

This declares an array of 3 rows, where each row holds 4 int values. That is 12 int values in total.

How it is stored: row-major order

The values are laid out row by row, one after another, in a single continuous block of memory. This layout is called row-major order: row 0 comes first, then row 1, then row 2.

You access an element with two indices:

grid[r][c]   // row r, column c

The first index (r) selects the row. The second index (c) selects the column within that row.

The inner dimension must be known

The inner dimension (the number of columns) must be a compile-time constant. A variable-length array, allowed since C99, is the one exception.

This rule also matters when passing a 2D array to a function. The function needs to know the inner dimension at compile time:

void f(int g[][4], int rows);

Here the column count (4) is fixed in the parameter, while the number of rows is passed separately.

Code examples

#include <stdio.h>

#define ROWS 3
#define COLS 4

/* Sum every element. The inner dimension (COLS) must be fixed so the
   compiler can locate g[r][c] = base + (r*COLS + c). */
long grid_sum(int g[][COLS], int rows) {
    long total = 0;
    for (int r = 0; r < rows; r++)
        for (int c = 0; c < COLS; c++)
            total += g[r][c];
    return total;
}

int main(void) {
    /* 3x4 grid, filled row by row at declaration time. */
    int grid[ROWS][COLS] = {
        { 1,  2,  3,  4},   // row 0
        { 5,  6,  7,  8},   // row 1
        { 9, 10, 11, 12}    // row 2
    };

    /* Print the grid as a table using nested loops. */
    for (int r = 0; r < ROWS; r++) {
        for (int c = 0; c < COLS; c++)
            printf("%3d ", grid[r][c]);   // %3d pads to width 3 for alignment
        printf("\n");
    }

    /* Show row-major layout: print the same 12 ints as one flat block. */
    int *flat = &grid[0][0];   // pointer to the first element
    printf("flat: ");
    for (int i = 0; i < ROWS * COLS; i++)
        printf("%d ", flat[i]);
    printf("\n");

    /* Demonstrate the offset formula for grid[2][1]. */
    int r = 2, c = 1;
    printf("grid[%d][%d] = %d, flat offset = %d, flat[%d] = %d\n",
           r, c, grid[r][c], r * COLS + c, r * COLS + c, flat[r * COLS + c]);

    printf("sum = %ld\n", grid_sum(grid, ROWS));
    return 0;
}

What it does. It builds a 3x4 grid, prints it as an aligned table, then prints the same values as one flat sequence to make row-major order visible, confirms that grid[2][1] lives at flat offset 2*4+1 = 9, and finally sums all elements through a function that takes the array (with a fixed column count).

Expected output:

  1   2   3   4 
  5   6   7   8 
  9  10  11  12 
flat: 1 2 3 4 5 6 7 8 9 10 11 12 
grid[2][1] = 10, flat offset = 9, flat[9] = 10
sum = 78

Edge cases. No dynamic memory is used, so there is nothing to free. If you read grid[3][0] or grid[0][4] you would go out of bounds (undefined behavior) — both indices must stay within their dimension. Using long for the sum guards against overflow when the grid holds many large values.

Line by line

  1. #define ROWS 3 / #define COLS 4 — name the dimensions once so the array, the loops, and the function parameter all agree.
  2. grid_sum(int g[][COLS], int rows) — the parameter keeps the column count (COLS) but takes the row count as a value, because only the inner dimension is needed for the r*COLS + c address math.
  3. Inside grid_sum, the nested loops add each g[r][c] to total. Visit order is (0,0),(0,1),...,(0,3),(1,0),... — exactly row-major order.
  4. In main, the initializer fills grid row by row: row 0 is 1 2 3 4, row 1 is 5 6 7 8, row 2 is 9 10 11 12.
  5. The first nested loop prints the table; %3d right-pads each number to width 3 so columns line up.
  6. int *flat = &grid[0][0]; takes the address of the very first element. Because the whole array is one contiguous block, walking flat[0..11] visits every element in storage order.
  7. The flat loop prints 1 2 3 ... 12, proving the values sit back-to-back regardless of the [r][c] notation.
  8. The offset demo computes r*COLS + c = 2*4 + 1 = 9 and shows grid[2][1], flat[9], and the literal value 10 all match.

Trace for the offset demo (r = 2, c = 1):

step                         value
r * COLS                     2 * 4 = 8
+ c                          8 + 1 = 9
flat[9]                      10   (the 10th element, 0-based)
grid[2][1]                   10   (same cell)
  1. grid_sum(grid, ROWS) passes the array (which decays to int (*)[COLS]) and the row count; the running total ends at 1+2+...+12 = 78.

Common mistakes

1. Swapping row and column order

Wrong:

int grid[ROWS][COLS];
grid[c][r] = value;   // indices reversed

Why it is wrong. The first index is the row; reversing them either reads a different cell or, if ROWS != COLS, indexes out of bounds. The compiler will not warn you.

Corrected:

grid[r][c] = value;   // row first, then column

Prevent it. Always say "row, column" aloud, and name loop variables r and c consistently.

2. Leaving the inner dimension empty in a function parameter

Wrong:

void f(int g[][], int rows);   // does not compile

Why it is wrong. Without the column count the compiler cannot compute r*COLS + c.

Corrected:

void f(int g[][COLS], int rows);   // inner size fixed

3. Loop bound mismatched to the dimension

Wrong:

for (int r = 0; r < COLS; r++)        // should be ROWS
    for (int c = 0; c < ROWS; c++)    // should be COLS
        grid[r][c] = 0;

Why it is wrong. When ROWS != COLS this reads/writes out of bounds and zeroes the wrong cells.

Corrected:

for (int r = 0; r < ROWS; r++)
    for (int c = 0; c < COLS; c++)
        grid[r][c] = 0;

4. Forgetting * cols when indexing a flat matrix

Wrong:

int v = m[r + c];        // missing the stride

Why it is wrong. Row r does not start at offset r; it starts at r * cols. This silently returns the wrong element.

Corrected:

int v = m[r * cols + c];

Prevent it. Wrap the math in a tiny helper: static inline int at(const int *m, int cols, int r, int c) { return m[r*cols + c]; }

Debugging tips

Compiler errors

  • "array type has incomplete element type" or a parse error on int g[][] — the inner dimension is missing; supply the column count: int g[][COLS].
  • "passing argument ... from incompatible pointer type" when calling a function — the function's column count does not match the array's. They must be identical.

Runtime / undefined behavior

  • A crash (segfault) or garbage values usually means an out-of-bounds index. Check that every row index is < ROWS and every column index is < COLS. Off-by-one (<= instead of <) is the classic cause.
  • Reading an uninitialized 2D array prints random values. Initialize with = { 0 } or fill it before use.

Logic errors

  • Output looks transposed or only part of the grid is touched -> your loop bounds are swapped (rows vs columns). Verify r is paired with ROWS and c with COLS.
  • Wrong cell read from a flat array -> you used r + c instead of r * cols + c.

How to debug

  1. Print the grid as a table with %3d to see the shape; misalignment reveals bad bounds quickly.
  2. Print the flat view (&grid[0][0] over ROWS*COLS) to confirm row-major storage matches your mental model.
  3. Use a debugger (gdb) or add printf showing r, c, and the computed offset to catch index mistakes.
  4. Build with warnings on: gcc -Wall -Wextra and, for memory errors, run under gcc -fsanitize=address.

Questions to ask when it doesn't work

  • Do my loop bounds match the right dimensions?
  • Are both indices within range for this array's size?
  • Did I initialize the array before reading it?
  • For a flat array, am I using r * cols + c?

Memory safety

A 2D array is one contiguous block, so the same safety rules as 1D arrays apply — there is just a second index to get wrong.

  • Bounds. Valid indices are 0 .. ROWS-1 for rows and 0 .. COLS-1 for columns. Accessing grid[ROWS][0], grid[0][COLS], or any negative index is out-of-bounds undefined behavior: it may crash, corrupt data, or appear to work by luck. C does not check this for you.
  • Initialization. A local 2D array is filled with garbage until you set it. Reading before writing is undefined. Use = { 0 } to zero the whole thing, or fill every cell first.
  • The whole block matters. Because storage is flat, an out-of-range column index can spill into the next row (still inside the array) and silently corrupt unrelated cells before it ever crashes — making the bug hard to spot.
  • Integer overflow in offsets. For large dimensions, r * cols + c (or rows * cols for a total) can overflow int. Use size_t for sizes and offsets when arrays get large.
  • Lifetime. Do not return a pointer to a 2D array (or one of its rows) that is a local variable — it ceases to exist when the function returns. Returning &local[0][0] is a dangling-pointer bug.

Defensive habits: keep the dimensions in named constants so loops, declarations, and parameters can never drift apart; validate any r/c that come from input before indexing; and compile with -Wall -Wextra -fsanitize=address while learning to catch out-of-bounds access immediately.

Real-world uses

Concrete use cases

  • Game boards and tile maps. Tic-tac-toe, chess, Minesweeper, and 2D game levels are stored as cell[ROWS][COLS]; the game logic loops over rows and columns to draw and update state.
  • Images. A grayscale image is a 2D array of pixel values; filters (blur, edge-detect) read a cell and its neighbors. Color images add a third dimension or pack channels per pixel.
  • Matrices. Graphics transforms, physics simulations, spreadsheets, and machine-learning weight tables are all 2D numeric arrays; performance code stores them flat and uses r * cols + c for cache-friendly, pointer-simple access.
  • Lookup tables. Distance-between-cities tables, scoring matrices, and state-transition tables map naturally to grids.

Professional best practices

Beginner rules:

  • Name the dimensions with #define or const/enum, and reuse those names everywhere.
  • Always index [row][column], and keep loop bounds matched to the right dimension.
  • Initialize before reading; validate indices that come from input.

Advanced habits:

  • Loop in row-major order (rows outer, columns inner) for cache locality on large arrays.
  • For runtime-sized matrices, prefer a single flat allocation with a small at(m, cols, r, c) accessor over arrays-of-pointers — it is contiguous, faster, and simpler to free.
  • Use size_t for sizes and offsets to avoid overflow; check multiplications (rows * cols) before allocating.
  • Document the layout (row-major) in the API so callers index consistently, especially when porting from column-major languages.

Practice tasks

Beginner

1. Fill and print a multiplication table.

  • Objective: build and display a 2D times-table.
  • Requirements: declare int table[5][5], fill table[r][c] = (r+1) * (c+1) with nested loops, then print it as an aligned grid using %4d.
  • Example (top-left corner): row 0 starts 1 2 3 4 5, row 1 starts 2 4 6 8 10.
  • Hints: outer loop over rows, inner over columns. Concepts: declaration, nested loops, [r][c] access.

2. Row sums.

  • Objective: compute the sum of each row of a fixed 3x4 grid.
  • Requirements: hard-code a 3x4 grid; for each row print row r sum = ....
  • Input/output: for rows {1,2,3,4}, {5,6,7,8}, {9,10,11,12}, print 12, 26, 42.
  • Hints: outer loop picks the row, inner loop accumulates that row's total (reset to 0 per row). Concepts: nested loops, accumulation.

Intermediate

3. Find the maximum and its position.

  • Objective: scan a 2D array for its largest value and report where it is.
  • Requirements: given a grid, print the max value and its (row, col). Handle ties by keeping the first occurrence.
  • Example: a grid whose largest entry 42 is at row 2, col 1 prints max 42 at (2,1).
  • Hints: initialize the best with grid[0][0] at (0,0), then compare every cell. Concepts: full traversal, tracking indices.

4. Transpose a square matrix.

  • Objective: turn m[r][c] into m[c][r] for an n x n matrix.
  • Requirements: write void transpose(int m[][N], int n) that swaps m[r][c] with m[c][r] in place; print before and after.
  • Constraint: only swap pairs once (loop c from r+1), or you will undo your own swaps.
  • Hints: a standard tmp swap. Concepts: row-major access, careful loop bounds, in-place modification.

Challenge

5. Flat-array matrix multiply.

  • Objective: multiply two matrices stored as flat 1D arrays in row-major order.
  • Requirements: implement void matmul(const int *a, const int *b, int *out, int n) for n x n matrices, where every element is accessed as x[r*n + c]. Compute out[i][j] = sum over k of a[i][k] * b[k][j].
  • Input/output: multiplying two 2x2 identity matrices yields the identity; test with small known matrices.
  • Constraints: do not use [][] syntax — use the flat r*n + c formula throughout. Watch for overflow (use long for the accumulator if values are large).
  • Hints: three nested loops (i, j, k); the inner loop sums products. Concepts: row-major offset math, nested loops, accumulation, function parameters.

Summary

  • A 2D array such as int grid[3][4] is an array of arrays: 3 rows of 4 ints, 12 ints total, stored in one contiguous block.
  • C uses row-major order — the whole first row, then the second, and so on. The flat offset of grid[r][c] is r * COLS + c.
  • Access cells as grid[row][column] (row first), and traverse with nested loops (rows outer, columns inner) for correctness and cache efficiency.
  • The inner (column) dimension must be a compile-time constant, including in function parameters: int g[][COLS]. Only the outer dimension may be omitted.
  • Runtime-sized matrices are often stored as a flat 1D array indexed by m[r * cols + c] — the exact math C does for you.
  • Most common mistakes: swapping row/column, mismatched loop bounds, omitting the inner dimension, and forgetting * cols when indexing a flat array. Keep dimensions in named constants, validate indices, initialize before reading, and stay in bounds — out-of-range access is undefined behavior.

Practice with these exercises