Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Pointers and 3D Arrays
A three-dimensional array is just an array of 2D arrays. See what each level of the pointer ladder points at, with real byte offsets from a program you can run.
Working with pointers and 3D arrays in C looks scary until you notice one pattern: every extra dimension just wraps the previous one in another array. In this lecture of our free C programming course you will name every level of a 3D array, see the type of each pointer expression, and verify the byte offsets by running a short program. This is the same thinking you use in embedded C programming when a buffer is organised by board, channel and sample.
What You Will Learn
- Why a 3D array is an array of 2D arrays, which are arrays of 1D arrays
- The size and type of every level
- What arr, *arr and **arr each point at
- How adding 1 behaves at every level
- Why a 3D array is not the same as an int ***
- When firmware actually needs three dimensions
Prerequisites
To follow pointers and 3D arrays in C you need the two previous lectures: pointer addresses in 2D arrays and subscripting a pointer to an array. Both are part of this free C tutorial series and take about ten minutes each.
Pointers and 3D Arrays in C: Building the Mental Model
Let us build a small example. A data logger has 2 ADC boards, each board has 2 channels, and each channel stores 3 samples.
int adc[2][2][3] = {
{ {1, 2, 3}, {4, 5, 6} },
{ {7, 8, 9}, {10, 11, 12} }
};
Read the declaration from the left. adc is an array of 2 elements. Each element is a board, which is an array of 2 channels. Each channel is an array of 3 ints. So the array is a stack of 2D arrays, and each 2D array is a stack of 1D arrays.
Size and type at every level
| Expression | Level | Size (4-byte int) | Type when used in an expression |
|---|---|---|---|
| adc | Whole array | 48 bytes | int (*)[2][3], pointer to a board |
| adc[i], *(adc+i) | One board | 24 bytes | int (*)[3], pointer to a channel |
| adc[i][j] | One channel | 12 bytes | int *, pointer to a sample |
| adc[i][j][k] | One sample | 4 bytes | int, the value |
Each time you apply * or a subscript you peel off the leftmost dimension, and the type loses one pair of brackets. This is why the step size changes at every level: stepping a board pointer moves 24 bytes, stepping a channel pointer moves 12, and stepping a sample pointer moves 4.
The Pointer Ladder for Pointers and 3D Arrays in C
Here is the full ladder, from the array name down to a single value. Memorise this table once and the long expressions stop looking mysterious.
| Expression | What it gives you |
|---|---|
| adc | Pointer to board 0 |
| adc + i | Pointer to board i |
| *(adc + i) | Board i itself, which decays to a pointer to its channel 0 |
| *(adc + i) + j | Pointer to channel j of board i |
| *(*(adc + i) + j) | Channel j of board i, which decays to a pointer to its sample 0 |
| *(*(adc + i) + j) + k | Pointer to sample k of channel j of board i |
| *(*(*(adc + i) + j) + k) | The value of that sample |
Demo: Measuring Every Level
The next program is the smallest demo of pointers and 3D arrays in C that still shows every level. It declares one pointer for each level and prints how far a single step moves it. Save it as ep_plane_walk.c.
#include <stdio.h>
#define BOARDS 2
#define CHANNELS 2
#define SAMPLES 3
int main(void)
{
/* adc[board][channel][sample] */
int adc[BOARDS][CHANNELS][SAMPLES] = {
{ {1, 2, 3}, {4, 5, 6} },
{ {7, 8, 9}, {10, 11, 12} }
};
const char *base = (const char *)adc;
printf("whole array : %zu bytes\n", sizeof adc);
printf("one board : %zu bytes\n", sizeof adc[0]);
printf("one channel : %zu bytes\n", sizeof adc[0][0]);
printf("one sample : %zu bytes\n", sizeof adc[0][0][0]);
int (*board)[CHANNELS][SAMPLES] = adc; /* walks boards */
int (*chan)[SAMPLES] = *adc; /* walks channels */
int *cell = **adc; /* walks samples */
printf("board+1 at +%td\n", (const char *)(board + 1) - base);
printf("chan+1 at +%td\n", (const char *)(chan + 1) - base);
printf("cell+1 at +%td\n", (const char *)(cell + 1) - base);
printf("*(adc+1) at +%td\n", (const char *)*(adc + 1) - base);
printf("*(adc+1)+1 at +%td\n", (const char *)(*(adc + 1) + 1) - base);
printf("**(adc+1) at +%td\n", (const char *)**(adc + 1) - base);
return 0;
}
gcc -std=c11 -Wall -Wextra -pedantic -o ep_plane_walk ep_plane_walk.c
./ep_plane_walk
Expected output (4-byte int):
whole array : 48 bytes
one board : 24 bytes
one channel : 12 bytes
one sample : 4 bytes
board+1 at +24
chan+1 at +12
cell+1 at +4
*(adc+1) at +24
*(adc+1)+1 at +36
**(adc+1) at +24
What each line tells you
- The sizes: 48 = 2 boards x 24, 24 = 2 channels x 12, 12 = 3 samples x 4.
- board+1 is +24: a board pointer jumps a whole board.
- chan+1 is +12 and cell+1 is +4: the same address, but the smaller the target type, the smaller the step.
- *(adc+1) is +24: board 1 starts right after the 24 bytes of board 0.
- *(adc+1)+1 is +36: inside board 1, one channel step (12 bytes) is added to 24.
- **(adc+1) is +24: sample 0 of channel 0 of board 1 is the first byte of board 1.
Pattern
The address stays the same as you dereference, only the type changes. The compiler uses the type to decide how big each step is.
A 3D Array Is Not an int ***
This is the biggest trap in the topic. Beginners see three subscripts and three stars and assume that int *** can stand in for a 3D array. It cannot. An int *** points at a pointer that points at a pointer that points at an int, and each of those objects can live anywhere in memory. A real 3D array is one block of contiguous ints, and the compiler computes addresses by arithmetic alone.
| Property | int adc[2][2][3] | int ***pp |
|---|---|---|
| Memory | One contiguous block | Scattered blocks linked by pointers |
| Address of [i][j][k] | Computed with multiplication and add | Found by following pointers |
| Extra storage for pointers | None | Many pointer variables |
| Passable to a function taking int *** | No | Yes |
When Firmware Uses Pointers and 3D Arrays in C
- Image or sensor frames with rows, columns and colour channels.
- Multi-board or multi-sensor data logging: board, channel, sample.
- History buffers for an IMU: axis, window, filter stage.
- Calibration tables indexed by temperature range, gain setting and channel.
Keep an eye on memory. A large 3D array declared as an ordinary local variable lives on the stack, and small microcontrollers have very little stack. Declare big tables static or global, and mark read-only tables const so the linker can place them in flash.
Common Mistakes With Pointers and 3D Arrays in C
| Mistake | Result | Fix |
|---|---|---|
| Treating int *** as a 3D array | Wrong addresses, crashes | Use int (*)[C][S] for the board pointer |
| Forgetting that each level has its own step size | Off-by-one-block reads | Check the type at every level |
| Mixing the order of dimensions | Samples read as channels | Name the dimensions with constants and keep the order in comments |
| Large local 3D array in an embedded function | Stack overflow | Make it static or global |
Best Practices for Pointers and 3D Arrays in C
- Declare each dimension with a named constant, never a bare number.
- Add a comment with the meaning of the dimensions in order, for example board, channel, sample.
- Prefer the subscript form in production code; use pointer form for understanding and debugging.
- Performance: iterate with the last index in the innermost loop so that memory is read in order.
- Safety: validate board and channel numbers that come from outside the program before indexing.
Summary and Key Takeaways
- A 3D array is an array of 2D arrays, stored contiguously.
- adc, *adc and **adc point at a board, a channel and a sample, with steps of 24, 12 and 4 bytes in our example.
- Each dereference removes one dimension from the type.
- *(*(*(adc+i)+j)+k) is the long way of writing adc[i][j][k].
- A 3D array is not an int ***, and the two are not interchangeable.
That is the full picture of pointers and 3D arrays in C. In the next lecture we turn this ladder into a loop that visits every element and prove the address formula with code. Keep going through this free embedded C course to finish the chapter.
Frequently Asked Questions
How do pointers work with 3D arrays in C?
The array name decays to a pointer to its first 2D block. Each dereference removes one dimension, so the step size shrinks from a board to a channel to a single element.
Is a 3D array the same as an int ***?
No. A 3D array is one contiguous block of values. An int *** is a chain of pointers. Their memory layout and types are different.
What is the type of the array name for int a[2][3][4]?
In an expression it decays to int (*)[3][4], a pointer to a 3 by 4 block.
How many bytes does a[2][2][3] of int occupy?
With 4-byte ints it is 2 x 2 x 3 x 4 = 48 bytes. The product of all dimensions times sizeof(int).
Why does *(adc+1) print the same offset as **(adc+1)?
Dereferencing changes the type but not the address. Both point to the first byte of board 1.
Should I use 3D arrays in embedded C programming?
Yes, when the data really has three natural dimensions and fixed sizes. Place large ones in static or flash memory instead of on the stack.
Free Embedded C Course
Every lecture in this free C programming course is tested code with real output. Continue to the next lecture or open the course index to jump to any topic.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

1 Comment