Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
3D Array Pointer Notation
Turn the pointer ladder into working loops, prove the row-major address formula with real output, and learn the flat-buffer technique used in real firmware.
Mastering 3D array pointer notation means you can read any long expression such as *(*(*(adc+i)+j)+k) at a glance and know the byte it touches. In this lecture of our free C programming course we visit every element with pointer notation, check the address formula against real output, and then see the flat-buffer alternative that many embedded C programming teams prefer. All code is tested and the expected output is shown.
What You Will Learn
- Why arr[i][j][k] equals *(*(*(arr+i)+j)+k)
- The row-major address formula and how to verify it
- How to loop over a 3D array with pointer notation
- How to pass one board of a 3D array to a function
- How a flat 1D buffer with an index macro replaces a 3D array
- Why walking across inner-array boundaries with one pointer is risky
Prerequisites
To learn 3D array pointer notation, read the previous lecture on pointers and 3D arrays first, because we reuse the same adc[2][2][3] example (boards, channels, samples). The tools are the same compiler and terminal that you have used throughout this free C tutorial.
Why the Subscript Form Equals 3D Array Pointer Notation
The C language defines E1[E2] to mean *(E1 + E2). Apply that rule three times, from the left, and you get the pointer notation for any 3D access.
Each rewrite removes one pair of square brackets and adds one star. When no brackets remain, you are holding the pointer-notation form. This works for any number of dimensions, so a 4D array is just one more round of the same rule.
The Row-Major Formula Behind 3D Array Pointer Notation
The compiler never stores the dimensions at run time. It turns every subscript into a single offset from the start of the array. For adc[BOARDS][CHANNELS][SAMPLES] the byte offset of element [i][j][k] is:
offset = ((i * CHANNELS + j) * SAMPLES + k) * sizeof(int)
Take element [1][1][2], the value 12. The index is (1 x 2 + 1) x 3 + 2 = 11, so the offset is 11 x 4 = 44 bytes. That is the last int of the 48-byte array.
| Element | Flat index | Byte offset | Value |
|---|---|---|---|
| adc[0][0][0] | 0 | 0 | 1 |
| adc[0][1][1] | 4 | 16 | 5 |
| adc[1][0][2] | 8 | 32 | 9 |
| adc[1][1][2] | 11 | 44 | 12 |
Notice what is missing
The number of boards (the first dimension) never appears in the formula. It only matters for bounds, not for addressing. That is why a function parameter may leave the first dimension blank but must give all the others.
Demo: Looping With 3D Array Pointer Notation
This program visits every element with the full pointer expression, checks each address against the formula above, compares the value with the subscript form, and passes one board to a function. Save it as ep_cube_walk.c.
#include <stdio.h>
#include <stddef.h>
#define BOARDS 2
#define CHANNELS 2
#define SAMPLES 3
/* Sum every sample of one board. Note the parameter type. */
static int board_total(int (*b)[SAMPLES], size_t channels)
{
int t = 0;
for (size_t c = 0; c < channels; c++)
for (size_t s = 0; s < SAMPLES; s++)
t += *(*(b + c) + s);
return t;
}
int main(void)
{
int adc[BOARDS][CHANNELS][SAMPLES] = {
{ {1, 2, 3}, {4, 5, 6} },
{ {7, 8, 9}, {10, 11, 12} }
};
const char *base = (const char *)adc;
int mismatches = 0;
for (int i = 0; i < BOARDS; i++) {
for (int j = 0; j < CHANNELS; j++) {
printf("board %d channel %d:", i, j);
for (int k = 0; k < SAMPLES; k++) {
int v = *(*(*(adc + i) + j) + k);
const int *where = *(*(adc + i) + j) + k;
ptrdiff_t off = (const char *)where - base;
ptrdiff_t expect = (ptrdiff_t)(((i * CHANNELS + j) * SAMPLES + k)
* (int)sizeof(int));
if (off != expect || v != adc[i][j][k])
mismatches++;
printf(" %d", v);
}
printf("\n");
}
}
printf("offset formula mismatches: %d\n", mismatches);
for (int i = 0; i < BOARDS; i++)
printf("total of board %d = %d\n", i, board_total(adc[i], CHANNELS));
return 0;
}
gcc -std=c11 -Wall -Wextra -pedantic -o ep_cube_walk ep_cube_walk.c
./ep_cube_walk
Expected output:
board 0 channel 0: 1 2 3
board 0 channel 1: 4 5 6
board 1 channel 0: 7 8 9
board 1 channel 1: 10 11 12
offset formula mismatches: 0
total of board 0 = 21
total of board 1 = 57
How the program works
- The triple loop visits the elements in memory order, because the sample index varies fastest.
- The pointer where holds the address of each element. Subtracting the base gives its byte offset, which we compare with the formula. Zero mismatches means the formula is right.
- board_total(adc[i], CHANNELS) passes a single board. The expression adc[i] is a 2D block that decays to int (*)[3], matching the parameter.
- The totals are 1+2+3+4+5+6 = 21 and 7+8+9+10+11+12 = 57.
3D Array Pointer Notation in Function Parameters
To accept the whole array, the parameter type keeps every dimension except the first:
void process(int (*all)[CHANNELS][SAMPLES], size_t boards);
/* same as: void process(int all[][CHANNELS][SAMPLES], size_t boards); */
| You pass | Parameter type |
|---|---|
| adc (the whole array) | int (*)[CHANNELS][SAMPLES] |
| adc[i] (one board) | int (*)[SAMPLES] |
| adc[i][j] (one channel) | int * |
The Flat Buffer Alternative
Many embedded teams avoid multi-dimensional parameters entirely. They keep one plain one-dimensional array and compute the index with a macro that follows the same row-major formula. The result has the same memory layout, simple function signatures, and no confusion about types. It also works nicely with DMA and memcpy, because the buffer is one block with one length.
#include <stdio.h>
#define BOARDS 2
#define CHANNELS 2
#define SAMPLES 3
#define TOTAL (BOARDS * CHANNELS * SAMPLES)
/* Row-major index of [b][c][s] in a flat buffer. */
#define IDX(b, c, s) ((((b) * CHANNELS) + (c)) * SAMPLES + (s))
int main(void)
{
int flat[TOTAL];
for (int n = 0; n < TOTAL; n++)
flat[n] = n + 1;
printf("flat[IDX(1,0,2)] = %d (index %d)\n", flat[IDX(1, 0, 2)], IDX(1, 0, 2));
printf("flat[IDX(0,1,1)] = %d (index %d)\n", flat[IDX(0, 1, 1)], IDX(0, 1, 1));
for (int b = 0; b < BOARDS; b++) {
int sum = 0;
for (int c = 0; c < CHANNELS; c++)
for (int s = 0; s < SAMPLES; s++)
sum += flat[IDX(b, c, s)];
printf("board %d sum = %d\n", b, sum);
}
return 0;
}
gcc -std=c11 -Wall -Wextra -pedantic -o ep_flat_index ep_flat_index.c
./ep_flat_index
flat[IDX(1,0,2)] = 9 (index 8)
flat[IDX(0,1,1)] = 5 (index 4)
board 0 sum = 21
board 1 sum = 57
The sums match the previous program, which confirms that the flat buffer and the 3D array describe the same layout.
| Aspect | 3D array | Flat buffer with IDX |
|---|---|---|
| Readability of access | adc[i][j][k] is natural | flat[IDX(i,j,k)] is slightly longer |
| Function parameters | Needs the long pointer-to-array type | A plain int * and a length |
| Bounds checking | Per-dimension by you | One total length, easy to check |
| DMA and memcpy | Fine, but types get awkward | Natural, one block |
Why One Pointer Should Not Cross Rows
You may see code that takes &adc[0][0][0] and then walks 12 elements with one int *. It usually works, but strictly the C standard only permits pointer arithmetic inside a single array object, and the pointer’s own object here is one 3-element channel. Optimisers may take advantage of that rule. When you need to treat the data as one long sequence, declare a flat buffer and use an index macro, as shown above, rather than relying on this shortcut.
Common Mistakes in 3D Array Pointer Notation
| Mistake | Result | Fix |
|---|---|---|
| Swapping CHANNELS and SAMPLES in the formula | Wrong elements read | Write the formula from the declaration order |
| Missing parentheses in the IDX macro arguments | Wrong index when an expression is passed | Wrap each macro argument in parentheses |
| Passing adc[i] to a parameter of type int * | Type mismatch warning | Use int (*)[SAMPLES] or pass adc[i][j] |
| Leaving out a dimension other than the first | Compile error | Keep all later dimensions in the parameter |
Best Practices for 3D Array Pointer Notation
- Test your index formula once with a program like the mismatch counter above, then rely on it.
- Keep dimension constants in one header so that arrays, loops and macros always agree.
- Performance: the formula costs a few multiplies. Compilers turn them into shifts and adds when the sizes are powers of two, so consider sizes such as 4 or 8 for hot loops.
- Safety: check each index against its own dimension, not just against the total length, or a wrong channel can silently read the next board.
Summary and Key Takeaways
- Apply E1[E2] = *(E1+E2) three times to turn arr[i][j][k] into *(*(*(arr+i)+j)+k).
- The byte offset is ((i x CHANNELS + j) x SAMPLES + k) x sizeof(int).
- The first dimension never appears in the formula, so a function parameter may omit it.
- A flat buffer with an index macro has the same layout and simpler function signatures.
- Do not walk across inner-array boundaries with a single pointer.
With 3D array pointer notation and the flat-buffer alternative in hand, you can read or write any multi-dimensional access. The next topic changes direction: how arguments travel into functions. Stay with this free embedded C course for pointers and functions.
Frequently Asked Questions
What is the pointer notation for arr[i][j][k]?
It is *(*(*(arr+i)+j)+k). Each subscript is replaced by an addition and a dereference.
How does the compiler compute the address of a 3D element?
It uses the row-major formula: start + ((i x C + j) x S + k) x sizeof(element), where C and S are the second and third dimensions.
Why can a function leave out the first dimension?
The first dimension is never used to compute an address, only the later ones are. The compiler needs those later ones to know the size of each step.
Is a flat buffer better than a 3D array?
Neither is always better. Flat buffers give simpler parameters and suit DMA, while 3D arrays give cleaner access syntax. Pick one style per project and stay consistent.
Can I walk a 3D array with one int pointer?
It often works, but the standard does not guarantee it when you cross inner-array boundaries. Use a flat buffer if you need a single sequential walk.
Does this apply to embedded C programming on small MCUs?
Yes. The offset formula is plain integer arithmetic, so it is cheap, and it helps you compute memory-map sizes by hand.
Learn Embedded C for Free
Keep building your skills with this free C programming course made for embedded systems students. Continue with pointers and functions, or revisit the course index.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments