What are Four Ways to Access Arrays- Online Embedded C Programming Classes

Need Help or More Information?

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

PREV_LEC NEXT_LEC

Free Embedded C Course · Pointers · Lecture 20

Four Ways to Access Arrays

Learn embedded c programming the practical way: see how arr[i], *(arr+i), *(p+i) and p[i] reach the same memory, with a runnable demo and exact output. Part of our free c programming course.

4
Equivalent access forms
2
Runnable demos
C11
Tested standard

What You Will Learn

  • The four equivalent ways to reach one array element: arr[i], *(arr+i), *(p+i) and p[i]
  • How the compiler turns an index into a byte offset using the element size
  • How to walk a buffer with a start pointer and an end pointer, the style used in most drivers
  • The real differences between an array name and a pointer variable
  • How to print addresses and offsets portably instead of guessing

Prerequisites

  • Pointer declaration, the address operator and dereferencing (earlier lectures of this chapter)
  • Pointer arithmetic: adding an integer to a pointer scales by the element size
  • A working GCC on Linux (any recent version) or an ARM GCC toolchain for your board

Why Embedded C Programming Depends on Array Pointers

Open any real firmware project and count the buffers: an ADC sample window, a UART receive queue, a DMA descriptor list, a sine lookup table in flash. Each one is an array, and each one is touched through pointers, because functions cannot receive an array by value. That is why a free embedded c course has to spend real time on array pointers before it moves on to drivers. If you can read *(p + i) as fluently as arr[i], driver code stops looking like noise.

Buffer in firmwareTypical element typeHow code usually reaches it
ADC sample windowuint16_tIndex in a filter loop, pointer handed to a DSP routine
UART receive bytesuint8_tHead and tail pointers moving through a ring buffer
Sine or calibration tableconst int16_tIndex into a table that lives in flash
DMA descriptor lista structPointer to the first descriptor, then p + 1, p + 2

The Four Ways to Access an Array Element

Take a tiny array and a pointer to its first element. In the lectures so far you saw that the array name, used in an expression, turns into the address of element 0. So after int adc[5]; int *p = adc; both adc and p hold the same address, and four different expressions land on element i.

ExpressionHow to read itWhen you will meet it
adc[i]Subscript on the arrayPlain index loops, the clearest form
*(adc + i)Move i elements forward, then dereferenceWhat the compiler really builds underneath
*(p + i)Same thing through the pointer variableInside functions that only received a pointer
p[i]Subscript applied to a pointerReadable access to a pointer parameter

The matching address forms are &adc[i], adc + i, p + i and &p[i]. All four evaluate to the same number. The bracket notation is only shorthand: the C standard defines a[b] to mean *(a + b), which is why one form can always be rewritten as the other.

Curiosity worth knowing

Because a[b] is defined as *(a + b) and addition commutes, 2[adc] is legal C and equals adc[2]. Never write it in real code, but it proves that subscripting is just pointer arithmetic with nicer clothes.

How the Compiler Scales the Index

You never multiply by the element size yourself. For an element type of size S, the address of element i is base + i * S. On a typical 64-bit Linux build int is 4 bytes, so element 3 sits 12 bytes after element 0. On an 8-bit microcontroller uint16_t is 2 bytes, so the same rule gives a 2-byte step. The C source stays identical; only the generated machine code changes.

Array adc[5] with 4-byte int elements
adc + 0120 byte offset 0 same cell as adc[0], p[0]
adc + 1340 byte offset 4
adc + 2560 byte offset 8
adc + 3780 byte offset 12
adc + 4900 byte offset 16
p points to the first cell; p + 3 lands on the cell holding 780

Demo 1: Reading One Element Four Ways

This program reads every element with all four forms and checks that the address expressions agree. Instead of printing raw addresses it prints the byte distance from the start of the array. Raw addresses change on every run because of address space randomisation, while the distance stays stable, so your output will match the listing below.

#include <stdio.h>

int main(void)
{
    int adc[5] = {120, 340, 560, 780, 900};
    int *p = adc;

    for (int i = 0; i < 5; i++) {
        int same = (&adc[i] == adc + i) && (adc + i == p + i);
        printf("i=%d offset=%td same_addr=%s | values: %d %d %d %d\n",
               i, (char *)(adc + i) - (char *)adc, same ? "yes" : "no",
               adc[i], *(adc + i), *(p + i), p[i]);
    }
    return 0;
}

Build and run it with these commands:

gcc -std=c11 -Wall -Wextra -o e20 e20.c
./e20

Expected output on a 64-bit Linux machine where int is 4 bytes:

i=0 offset=0 same_addr=yes | values: 120 120 120 120
i=1 offset=4 same_addr=yes | values: 340 340 340 340
i=2 offset=8 same_addr=yes | values: 560 560 560 560
i=3 offset=12 same_addr=yes | values: 780 780 780 780
i=4 offset=16 same_addr=yes | values: 900 900 900 900

Notice the casts to char * before subtracting. Subtracting two int * values gives a count of elements, not bytes. Casting to char * makes the subtraction count bytes, which is exactly what we want to show. Many older textbooks print addresses with %u and assume a 16-bit int. That is undefined behaviour on modern systems; if you ever need the raw value, use %p with a (void *) cast.

Demo 2: Walking a Buffer With Two Pointers

Real drivers rarely index with a counter. They keep a pointer to the first element and a pointer one past the last, and loop until the two meet. That is the convention used by the C++ standard library and by a lot of embedded c programming, and it avoids carrying a separate length around.

#include <stdio.h>

static int sum_samples(const int *start, const int *end)
{
    int total = 0;
    for (const int *cur = start; cur != end; cur++)
        total += *cur;
    return total;
}

int main(void)
{
    int adc[5] = {120, 340, 560, 780, 900};
    int n = (int)(sizeof adc / sizeof adc[0]);

    printf("sum = %d\n", sum_samples(adc, adc + n));
    printf("elements between = %td\n", (adc + 4) - adc);
    return 0;
}
gcc -std=c11 -Wall -Wextra -o e20b e20b.c
./e20b
sum = 2700
elements between = 4

The end pointer adc + n points just past the last element. The standard allows you to form that address and compare against it, but you must never dereference it. The second line shows that subtracting two pointers into the same array gives the number of elements between them, here (adc + 4) - adc which is 4, not 16 bytes.

Start and end pointers over five samples
start 120340560780900 end (one past, never read)
cur moves right one element per loop pass until cur == end

Array Name Versus Pointer Variable

Although the two behave alike inside expressions, they are different objects. This table is worth memorising before an interview.

PropertyArray name adcPointer variable p
Can you write ++ on it?No, it is not a modifiable lvalueYes, it simply moves forward
sizeof result (64-bit)20, the whole array8, just the pointer
Occupies storage for an address?No, the address is computedYes, one pointer-sized object
&name meansAddress of the whole arrayAddress of the pointer variable

Watch out for array parameters

When a function is declared as void f(int a[5]), the compiler rewrites it to void f(int *a). Inside the function sizeof a gives the pointer size, not 20. Always pass the length as a separate argument.

Common Mistakes in Embedded C Programming With Arrays

MistakeWhat you seeFix
Dereferencing the end pointerGarbage value or a hard fault on the last passStop when cur == end; read only before incrementing past it
Writing adc++ on an arrayCompile error: not an lvalueCopy it into a pointer first: int *p = adc;
Using sizeof on a pointer parameterLoop runs 2 times instead of 5Pass the element count explicitly
Subtracting pointers and expecting bytesResult is 4 where you expected 16Cast to char * or multiply by sizeof
Printing pointers with %d or %uTruncated or wrong value on 64-bitUse %p and cast to (void *)

Embedded C Programming Best Practices, Performance and Security

Performance

Performance first. In embedded c programming the compiler does most of the work for you. With optimisation on (-O2), GCC compiles adc[i], *(adc + i) and a pointer walk into nearly the same load instructions, so choose the form that reads best. On very small 8-bit parts a moving pointer can save a multiply or an add per pass, but measure on your target before rewriting working code.

Security and Safety

Neither form checks bounds. An index of 5 on a five-element array silently reads or overwrites the neighbouring variable, which is how many buffer overflows in firmware start. Keep the length next to the pointer, mark read-only data const, and compile with -Wall -Wextra. Coding standards such as MISRA C restrict pointer arithmetic for exactly this reason, so know the rule before you adopt a fancy trick.

  • Prefer arr[i] when you are indexing and start/end pointers when you are scanning.
  • Use const on pointers that only read, such as the sum_samples parameters above.
  • Compute the element count once with sizeof arr / sizeof arr[0] in the scope where the array is defined.
  • Never keep a pointer to a local array after the function returns.

Real-World Embedded C Programming With Array Pointers

  • Moving average filter: a pointer walks the last N ADC samples and a running sum is updated per pass.
  • Ring buffer: head and tail pointers wrap around the end of a fixed array inside a UART interrupt handler.
  • Command parser: a read pointer advances through received bytes while a state machine decides what each byte means.
  • Lookup tables: a const table in flash is read with a computed index, with no run-time cost beyond one load.

Key Takeaways and Conclusion

Here is the short version to keep in your notes for embedded c programming interviews and code reviews.

  • arr[i], *(arr+i), *(p+i) and p[i] reach the same element; the address is base + i * sizeof(element).
  • An array name is not a pointer variable: it cannot be incremented and sizeof sees the whole array.
  • Subtracting two pointers counts elements; cast to char * if you want bytes.
  • The end pointer may be formed and compared, never dereferenced.

You now know the four spellings of the same operation, which is the foundation for everything that follows in this free c tutorial. Once these access forms feel natural, the next step is the more unusual idea of a pointer that points to an entire array rather than to one element. That pointer type drives how C handles fixed-size frames and two-dimensional tables, and it is a favourite topic in embedded c programming interviews.

Frequently Asked Questions

Is arr[i] slower than *(arr + i)?

No. They mean exactly the same thing in the C standard, and any modern compiler produces identical machine code for them.

Why does sizeof give 20 for the array but 8 for the pointer?

The array object really occupies 5 elements of 4 bytes, so sizeof reports 20. The pointer is a separate object that only stores an address, which is 8 bytes on a 64-bit target.

Can I increment an array name like adc++?

No. The array name is not a modifiable lvalue, so the compiler rejects it. Copy the address into a pointer variable and increment that instead.

Why is the end pointer allowed to point one past the last element?

The standard explicitly permits forming and comparing that address so loops can test against it. Dereferencing it is undefined behaviour.

Why do I get a different address every time I run the program?

Linux randomises the stack layout for security. Print offsets between addresses, as in Demo 1, when you want stable output.

Which form should I use in firmware?

Use the one that makes the code easiest to read. Indexing suits counted loops, and start/end pointers suit scanning a buffer you do not know the length of in advance.

Do these rules change on a microcontroller?

The language rules are identical. Only the element sizes change, for example a 2-byte uint16_t step on an 8-bit or 32-bit part.

Keep Going With the Free C Tutorial

Next we meet a pointer that points to a whole array instead of one element. Continue the free embedded c course and keep building your pointer skills.

PREV_LEC NEXT_LEC

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 *