Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Multidimensional Array Pointer Arithmetic
See why arr, arr[0] and &arr[0][0] print the same address but behave differently, how big each pointer step really is in 2-D and 3-D arrays, and how to walk a table as one flat block. Free C tutorial for embedded systems.
The same address can mean very different things in C. In a two-dimensional array, arr, arr[0] and &arr[0][0] all begin at the same byte, yet adding 1 to each moves you by a row, an element, or something else entirely. Understanding pointer arithmetic on multidimensional arrays in C is a core skill for embedded C programming, where lookup tables, image buffers and calibration matrices are everywhere.
This lecture of our free C programming course answers the textbook question “what do these printf calls print?” by letting the compiler tell us. Instead of guessing a base address, we measure byte differences, which are the same on every machine with the same int size.
What You Will Learn
- Row-major memory layout for 2-D and 3-D arrays
- The type of
arr,arr[0],arr[0][0]and what each one points to - How sizeof behaves at each level of the array
- How much
+ 1moves each pointer - The correct way to get a flat
int *over a 2-D array, and the caveat
Prerequisites
You should understand one-dimensional array and pointer equivalence, and the array decay rules from the previous lecture. We use int of 4 bytes in the sample output. If your toolchain uses a different size, scale the numbers accordingly.
How a 2-D Array Sits in Memory
C stores a two-dimensional array in row-major order. An array declared as int m[3][4] is, in memory, just twelve ints in a row. The first four belong to row 0, the next four to row 1, and so on. There are no hidden pointers between rows. The array-of-arrays structure exists only in the type system.
Read the three rows above as one continuous strip from top to bottom. The compiler turns m[r][c] into an offset of r * 4 + c elements from the start.
Types at Every Level
| Expression | Type after decay | Points to | sizeof the pointed-to object | + 1 moves by |
|---|---|---|---|---|
m | int (*)[4] | Row 0 (4 ints) | 16 bytes | 16 bytes |
m[0] | int * | Element m[0][0] | 4 bytes | 4 bytes |
&m[0][0] | int * | Element m[0][0] | 4 bytes | 4 bytes |
&m | int (*)[3][4] | The entire array | 48 bytes | 48 bytes |
Same address, different type
The numeric value of m, m[0] and &m[0][0] is identical. The type, and therefore the step size of pointer arithmetic, is not. The compiler uses the type to decide how many bytes + 1 adds.
Measure It: Step Sizes in 2-D and 3-D
Save this program as ep_multi_dim.c. It declares a 3 by 4 array and a 3 by 4 by 5 array, prints sizes at each level, and uses casts to char * so subtracting two pointers gives a byte count.
#include <stdio.h>
int main(void)
{
int m[3][4];
int c[3][4][5];
printf("m total/row/elem : %zu %zu %zu\n",
sizeof m, sizeof m[0], sizeof m[0][0]);
printf("c total/plane/row/elem : %zu %zu %zu %zu\n",
sizeof c, sizeof c[0], sizeof c[0][0], sizeof c[0][0][0]);
printf("m+1 jumps %td bytes\n", (char *)(m + 1) - (char *)m);
printf("m[0]+1 jumps %td bytes\n", (char *)(m[0] + 1) - (char *)m[0]);
printf("c+1 jumps %td bytes\n", (char *)(c + 1) - (char *)c);
printf("c[0]+1 jumps %td bytes\n", (char *)(c[0] + 1) - (char *)c[0]);
printf("c[0][0]+1 jumps %td bytes\n", (char *)(c[0][0] + 1) - (char *)c[0][0]);
printf("same start address? %d %d\n",
(void *)m == (void *)m[0], (void *)m[0] == (void *)&m[0][0]);
int k = 10;
for (int r = 0; r < 3; r++)
for (int q = 0; q < 4; q++)
m[r][q] = k++;
int *flat = &m[0][0];
for (int i = 0; i < 12; i++)
printf("%d ", flat[i]);
printf("\n");
return 0;
}
Build and Run
$ gcc -Wall -Wextra -std=c11 -o ep_multi_dim ep_multi_dim.c
$ ./ep_multi_dim
m total/row/elem : 48 16 4
c total/plane/row/elem : 240 80 20 4
m+1 jumps 16 bytes
m[0]+1 jumps 4 bytes
c+1 jumps 80 bytes
c[0]+1 jumps 20 bytes
c[0][0]+1 jumps 4 bytes
same start address? 1 1
10 11 12 13 14 15 16 17 18 19 20 21
Reading the Results
Each time you remove one subscript, you go one level deeper and the unit shrinks. c + 1 skips a whole 80-byte plane, c[0] + 1 skips a 20-byte row, and c[0][0] + 1 skips a single 4-byte int. The “same start address” line prints 1 1, confirming that all those names begin at the same byte.
A Flat Pointer Over a 2-D Array
Older exercises sometimes write int *p = arr; for a 2-D array and then loop over twelve elements. Modern compilers warn that the types are incompatible, because arr decays to int (*)[4], not int *. The same applies to writing int *p = &arr;, whose type is int (*)[3][4]. Both only appear to work because the address is the same.
The warning-free spelling is int *flat = &m[0][0];, as in the program, which printed the twelve values in order. Row 1 follows row 0 immediately, so a table can be walked as one list.
flat[i] is valid inside the first row, because the pointer was derived from m[0][0]. Every mainstream compiler handles the flat walk correctly and it is widely used, but in safety-critical code prefer nested row and column loops, or declare the data as a one-dimensional array and compute the index yourself.
Real-World Uses in Embedded Systems
- Calibration and lookup tables. A 2-D table of gain values indexed by temperature band and channel. Knowing the row size lets you jump to a band with one pointer addition.
- Display frame buffers. A small monochrome display stored as
uint8_t fb[8][128]. Sending one page to the controller means passingfb[page]and a length of 128. - Matrix maths for sensors. Rotation matrices for IMU data are small fixed-size 3 by 3 arrays.
- Keypad scan tables. Row and column codes stored in a 2-D table, one row per scan state.
Common Mistakes and Troubleshooting
| Mistake | Why it goes wrong | Fix |
|---|---|---|
Assuming m + 1 moves one int | m points to a row, so it moves one row | Use *m + 1 or m[0] + 1 for the next element |
Assigning m to int * | Incompatible pointer types, warning | Use &m[0][0] or int (*p)[4] |
Printing addresses with %u | Wrong format for pointers, truncation on 64-bit | Use %p with a void * cast, or print byte differences |
| Swapping row and column sizes in a declaration | Table reads wrong data | Remember the last subscript is the contiguous one |
Passing int ** to a function expecting a 2-D array | A true 2-D array is not an array of pointers | Declare the parameter as int (*m)[4] |
Best Practices
- Make the column count a named constant so the type, the loops and the function parameters all agree.
- Use
conston lookup tables so they land in flash, not RAM, on most microcontrollers. - Walk the last dimension in the innermost loop. It matches the memory order and is friendlier to caches.
- Print byte distances or element indices when you need reproducible output.
- When in doubt about a type, print
sizeofof the expression. It reveals what level you are at.
Performance and Security Considerations
Row-major order means looping rows in the outer loop and columns in the inner loop reads memory sequentially, which is faster on any machine with a cache and kinder to DMA bursts. For security and correctness, index arithmetic on multidimensional arrays is a common source of off-by-one overflow, so keep each bound explicit and validated.
Suggested Images
| Image idea | ALT text |
|---|---|
| Rows of a 2-D array laid end to end as one memory strip | Row-major layout for pointer arithmetic on multidimensional arrays in C |
| Nested boxes showing plane, row and element sizes | 3-D array sizeof levels in a free C programming course |
| Terminal showing 16, 4, 80, 20 byte jumps | Embedded C programming pointer step sizes for 2D and 3D arrays |
Summary and Key Takeaways
Remember
- Multidimensional arrays are stored row-major as one contiguous block.
m,m[0]and&m[0][0]share an address but differ in type and step size.- Each subscript removed moves you one level deeper and shrinks the unit.
int *p = arr;on a 2-D array is a type mismatch. Use&arr[0][0].- Measure, do not guess. Byte differences and sizeof give you the truth.
Conclusion
Once you read every array expression as “an address plus a type”, the apparently contradictory answers in pointer exercises become predictable. The address is shared and the type decides the stride. The next lecture builds on this by comparing an array of pointers with a pointer to an array, two declarations that look nearly identical and mean very different things.
FAQ: Multidimensional Arrays and Pointers in C
Why do arr and arr[0] print the same address?
Because the first row starts at the same byte as the whole array. Their types differ, so adding 1 moves them by different amounts.
What is the difference between arr and &arr for a 2-D array?
arr decays to a pointer to the first row. &arr is a pointer to the whole array, so adding 1 skips every row.
Is a 2-D array the same as an array of pointers?
No. A 2-D array is one contiguous block with no stored row pointers. An array of pointers holds addresses that may point anywhere.
Why does my compiler warn about int *p = arr?
For a 2-D array, arr decays to a pointer to an array of ints, which is a different type from a pointer to int.
How do I pass a 2-D array to a function?
Declare the parameter with the column count, for example int m[][4] or int (*m)[4], and pass the number of rows separately.
Free Embedded C Course for Students
Keep building: next we compare arrays of pointers with pointers to arrays, a favourite interview topic.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments