Arrays & Strings · beginner · ~10 min
- 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`).
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.
Grids and tables are everywhere in real software, and 2D arrays are the most direct way to represent them in C:
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.
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 manydoublevalues are stored in total, and what is the type oftable[0]?
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 ofgrid[1][3]? What aboutgrid[2][0]?
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;forint grid[ROWS][COLS]whereROWS != COLS. What goes wrong?
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 rejectint g[][]?
// 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:
g[i] (one index) gives you a whole row as an int[COLS], which decays to a pointer in most expressions.int g[][3] and int (*g)[3] mean the same thing: a pointer to an array of 3 ints (a row).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.
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 (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.
#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.
#define ROWS 3 / #define COLS 4 — name the dimensions once so the array, the loops, and the function parameter all agree.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.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.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.%3d right-pads each number to width 3 so columns line up.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.1 2 3 ... 12, proving the values sit back-to-back regardless of the [r][c] notation.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)
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.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.
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
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;
* cols when indexing a flat matrixWrong:
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]; }
Compiler errors
int g[][] — the inner dimension is missing; supply the column count: int g[][COLS].Runtime / undefined behavior
< ROWS and every column index is < COLS. Off-by-one (<= instead of <) is the classic cause.= { 0 } or fill it before use.Logic errors
r is paired with ROWS and c with COLS.r + c instead of r * cols + c.How to debug
%3d to see the shape; misalignment reveals bad bounds quickly.&grid[0][0] over ROWS*COLS) to confirm row-major storage matches your mental model.gdb) or add printf showing r, c, and the computed offset to catch index mistakes.gcc -Wall -Wextra and, for memory errors, run under gcc -fsanitize=address.Questions to ask when it doesn't work
r * cols + c?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.
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.= { 0 } to zero the whole thing, or fill every cell first.r * cols + c (or rows * cols for a total) can overflow int. Use size_t for sizes and offsets when arrays get large.&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.
Concrete use cases
cell[ROWS][COLS]; the game logic loops over rows and columns to draw and update state.r * cols + c for cache-friendly, pointer-simple access.Professional best practices
Beginner rules:
#define or const/enum, and reuse those names everywhere.[row][column], and keep loop bounds matched to the right dimension.Advanced habits:
at(m, cols, r, c) accessor over arrays-of-pointers — it is contiguous, faster, and simpler to free.size_t for sizes and offsets to avoid overflow; check multiplications (rows * cols) before allocating.1. Fill and print a multiplication table.
int table[5][5], fill table[r][c] = (r+1) * (c+1) with nested loops, then print it as an aligned grid using %4d.1 2 3 4 5, row 1 starts 2 4 6 8 10.[r][c] access.2. Row sums.
row r sum = ....{1,2,3,4}, {5,6,7,8}, {9,10,11,12}, print 12, 26, 42.3. Find the maximum and its position.
(row, col). Handle ties by keeping the first occurrence.42 is at row 2, col 1 prints max 42 at (2,1).grid[0][0] at (0,0), then compare every cell. Concepts: full traversal, tracking indices.4. Transpose a square matrix.
m[r][c] into m[c][r] for an n x n matrix.void transpose(int m[][N], int n) that swaps m[r][c] with m[c][r] in place; print before and after.c from r+1), or you will undo your own swaps.tmp swap. Concepts: row-major access, careful loop bounds, in-place modification.5. Flat-array matrix multiply.
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].[][] syntax — use the flat r*n + c formula throughout. Watch for overflow (use long for the accumulator if values are large).i, j, k); the inner loop sums products. Concepts: row-major offset math, nested loops, accumulation, function parameters.int grid[3][4] is an array of arrays: 3 rows of 4 ints, 12 ints total, stored in one contiguous block.grid[r][c] is r * COLS + c.grid[row][column] (row first), and traverse with nested loops (rows outer, columns inner) for correctness and cache efficiency.int g[][COLS]. Only the outer dimension may be omitted.m[r * cols + c] — the exact math C does for you.* 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.