Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
2D Arrays in Memory
See how a grid becomes one strip of bytes in row-major order. This free embedded c course lecture proves it with a runnable offset printer and real keypad and LED matrix examples.
What You Will Learn
- Why a two-dimensional array is really one continuous strip of memory stored in row-major order
- How to see a 2-D array as an array of arrays, and what that means for
sizeof - The address formula
base + (i * COLS + j) * sizeof(element) - How to compute the row and column counts without magic numbers
- Why loop order affects speed and what embedded tables look like in real firmware
Prerequisites
- One-dimensional arrays, subscripts and initialisation
- The previous lectures on pointers to arrays and their dereferencing
- GCC on Linux or any ARM GCC toolchain
Free Embedded C Course: How a 2D Array Is Stored in Memory
Welcome to the memory chapter of our free embedded c course. We draw a two-dimensional array as a rectangle of rows and columns because that is how humans think about a keypad, an LED matrix or a calibration table. Memory, however, has no rectangles. It is a single long list of byte addresses, so the compiler must flatten the grid into one line.
C flattens it in row-major order: the whole of row 0 first, then the whole of row 1, then row 2, with no gaps between them. The rectangle exists only in your source code and in the compiler’s index arithmetic.
A 2D Array Is an Array of Arrays
Here is the declaration used throughout this lecture:
uint16_t grid[3][4];
Read it as: grid is an array of 3 elements, and each element is itself an array of 4 uint16_t. That reading is not just a mental trick; it is exactly how the type system sees it. grid[1] is a complete row, a uint16_t[4] object, and grid[1][2] picks one element out of that row.
| Expression | Type | Size on 64-bit Linux |
|---|---|---|
grid | uint16_t[3][4] | 24 bytes (12 elements of 2 bytes) |
grid[1] | uint16_t[4] | 8 bytes (one row) |
grid[1][2] | uint16_t | 2 bytes (one element) |
The Address Formula
Because rows sit one after another, the address of grid[i][j] is base + (i * COLS + j) * sizeof(element). The compiler turns your two indexes into that single calculation. Try it for grid[2][1]: (2 * 4 + 1) * 2 = 18 bytes from the start.
Demo: Printing Every Offset
Run this demo as part of the free embedded c course practice. It prints each element with its byte offset from the first element, plus the sizes from the table. It also shows the safe idiom for counting rows and columns without hard-coding numbers.
#include <stdio.h>
#include <stdint.h>
int main(void)
{
uint16_t grid[3][4] = {
{101, 102, 103, 104},
{201, 202, 203, 204},
{301, 302, 303, 304}
};
char *base = (char *)&grid[0][0];
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++)
printf("grid[%d][%d]=%u @+%02td ", i, j, grid[i][j],
(char *)&grid[i][j] - base);
printf("\n");
}
printf("sizeof(grid) = %zu\n", sizeof grid);
printf("sizeof(grid[0]) = %zu\n", sizeof grid[0]);
printf("rows = %zu, cols = %zu\n", sizeof grid / sizeof grid[0],
sizeof grid[0] / sizeof grid[0][0]);
return 0;
}
gcc -std=c11 -Wall -Wextra -o e23 e23.c
./e23
Expected output (the lines are wide, so they may wrap in your terminal):
grid[0][0]=101 @+00 grid[0][1]=102 @+02 grid[0][2]=103 @+04 grid[0][3]=104 @+06
grid[1][0]=201 @+08 grid[1][1]=202 @+10 grid[1][2]=203 @+12 grid[1][3]=204 @+14
grid[2][0]=301 @+16 grid[2][1]=302 @+18 grid[2][2]=303 @+20 grid[2][3]=304 @+22
sizeof(grid) = 24
sizeof(grid[0]) = 8
rows = 3, cols = 4
The offsets climb by 2 with no break: 0, 2, 4 and so on up to 22. There is no padding between the end of one row and the start of the next, so a 3 by 4 grid is exactly twelve consecutive elements. The last line shows the idiom: total size divided by the size of one row gives the row count, and the size of one row divided by the size of one element gives the column count.
Row-Major Order and Why Embedded Developers Care
Every learner of this free embedded c course eventually hits a performance question, and the layout answers it. Knowing the layout pays off in three practical ways. First, you can treat a 2-D buffer as one flat buffer when a peripheral needs it, for example sending a display frame to a DMA controller in a single transfer. Second, loop order matters. Walking along a row touches neighbouring addresses, which suits caches and prefetchers. Walking down a column jumps a whole row each step.
/* Good: inner loop walks along a row, one element after another in memory */
for (int i = 0; i < ROWS; i++)
for (int j = 0; j < COLS; j++)
total += grid[i][j];
/* Poor: inner loop jumps a whole row (COLS elements) on every step */
for (int j = 0; j < COLS; j++)
for (int i = 0; i < ROWS; i++)
total += grid[i][j];
On a microcontroller without a cache the difference is smaller, but on application processors running Linux it can be large. Always keep the last index as the innermost loop.
A 2D array is not an int **
uint16_t grid[3][4] stores values directly, in one block. A uint16_t ** points at a list of separate row pointers. The two have different memory layouts, and passing one where the other is expected compiles into garbage or a crash. A function that accepts a 2-D array needs the column count in its type, which the next lecture shows.
Real-World 2D Tables for the Free Embedded C Course
Here is a keypad scan table of the kind found in every matrix-keypad driver. The table is const, so a linker places it in flash and it costs no RAM:
#define KEY_ROWS 3
#define KEY_COLS 4
static const char keymap[KEY_ROWS][KEY_COLS] = {
{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'}
};
char key_at(int row, int col)
{
return keymap[row][col]; /* address = keymap + row*KEY_COLS + col */
}
- Matrix keypad: the driver drives one row at a time and reads the column lines, then looks up the key in a table like the one above.
- LED matrix or display frame: each row is one scan line, and the whole frame is sent to the panel in row order.
- Calibration maps: a 2-D table indexed by temperature step and voltage step returns a correction value.
- Image or sensor grids: thermal sensor arrays deliver pixels row by row, matching row-major storage exactly.
Common 2D Array Mistakes in the Free Embedded C Course
| Mistake | Symptom | Fix |
|---|---|---|
Swapping the indexes: grid[col][row] | Wrong values or reads outside the array | Row first, column second, always |
| Initialising with the wrong number of columns | Values appear in unexpected slots | Use braces per row and compare against the declared column count |
Treating it as int ** | Crash or garbage when passed to a function | Use a pointer to a row, uint16_t (*)[4] |
| Hard-coding 3 and 4 in loops | Break when the table grows | Compute with sizeof or define ROWS and COLS once |
| Column-first loops on big tables | Slow code on cached processors | Keep the column index in the inner loop |
Best Practices, Performance and Security
- Define
ROWSandCOLSonce, and declare both the array and the loops from those names. - Mark read-only tables
constso they live in flash and cannot be changed by a stray write. - Use
sizeof grid / sizeof grid[0]for the row count so it updates when the table changes. - Check row and column indexes that come from external data before using them; an out-of-range index is undefined behaviour.
Production firmware written after any free embedded c course still cares about locality. Performance is mostly about locality: sequential row access is the fast path. On the safety side, remember that grid[3][0] is the first element past the end and grid[0][4] silently aliases grid[1][0] in practice, although the standard still calls it undefined. Never rely on that aliasing.
Key Takeaways and Conclusion
- A 2-D array is one strip of memory in row-major order, with no gaps between rows.
uint16_t grid[3][4]is an array of 3 rows, each auint16_t[4].- The address of
grid[i][j]isbase + (i * COLS + j) * sizeof(element). - Count rows and columns with
sizeofinstead of magic numbers, and keep the column index innermost in loops.
You can now picture exactly where every element of a two-dimensional table sits in memory, which is the knowledge that makes the pointer expressions in the next lecture easy rather than mysterious. If you are working through this free embedded c course, keep this layout picture handy. It also helps with your c programming for embedded systems interviews, where candidates are often asked to explain how a matrix is stored.
Frequently Asked Questions
How is a 2D array stored in memory in C?
In row-major order: all elements of row 0, then all of row 1, and so on, in one continuous block with no gaps between rows.
What is the address of grid[i][j]?
It is base + (i * COLS + j) * sizeof(element), where COLS is the number of columns declared for the array.
Is a 2D array the same as an array of pointers?
No. A 2-D array stores all values in one block. An array of pointers stores addresses that may point to separate blocks.
Why does the compiler need the column count but not the row count in a parameter?
To find an element it multiplies the row index by the row length, so it must know the columns. The number of rows only matters for bounds, which C does not check.
Which loop order is faster for a 2D array?
Keep the column index in the inner loop. That visits consecutive addresses, which is friendlier to caches and prefetching.
How do I get the number of rows and columns of an array?
Rows are sizeof grid / sizeof grid[0] and columns are sizeof grid[0] / sizeof grid[0][0], valid in the scope where the array is defined.
Can I treat a 2D array as a flat one-dimensional array?
Its memory is flat, so low-level code such as a DMA transfer can treat it that way. Take care to follow the aliasing rules if you access it through a different type.
Finish the Pointers Chapter
Next we combine pointers and 2D arrays: grid+1, *(grid+1) and *(*(grid+2)+3) finally make sense. Keep learning in our free c programming course.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments