What are 3D Array Pointer Notation- C Programming Tutorials For Beginners

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

Free C Programming Course · Pointers · Lecture 28

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.

14 min
Reading time
Intermediate
Level
2 demos
Compile and run

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.

Rewriting adc[i][j][k] one subscript at a time
Startadc[i][j][k]
Rewrite 1(*(adc+i))[j][k]
Rewrite 2(*(*(adc+i)+j))[k]
Rewrite 3*(*(*(adc+i)+j)+k)

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.

ElementFlat indexByte offsetValue
adc[0][0][0]001
adc[0][1][1]4165
adc[1][0][2]8329
adc[1][1][2]114412

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 passParameter 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.

Aspect3D arrayFlat buffer with IDX
Readability of accessadc[i][j][k] is naturalflat[IDX(i,j,k)] is slightly longer
Function parametersNeeds the long pointer-to-array typeA plain int * and a length
Bounds checkingPer-dimension by youOne total length, easy to check
DMA and memcpyFine, but types get awkwardNatural, 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.

Warning: code that crosses inner-array boundaries may pass every test on one compiler and break after an optimisation level or compiler upgrade. Use the flat-buffer approach for sequential walks.

Common Mistakes in 3D Array Pointer Notation

MistakeResultFix
Swapping CHANNELS and SAMPLES in the formulaWrong elements readWrite the formula from the declaration order
Missing parentheses in the IDX macro argumentsWrong index when an expression is passedWrap each macro argument in parentheses
Passing adc[i] to a parameter of type int *Type mismatch warningUse int (*)[SAMPLES] or pass adc[i][j]
Leaving out a dimension other than the firstCompile errorKeep 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

Leave a Reply

Your email address will not be published. Required fields are marked *