Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
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.
What You Will Learn
- The four equivalent ways to reach one array element:
arr[i],*(arr+i),*(p+i)andp[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 firmware | Typical element type | How code usually reaches it |
|---|---|---|
| ADC sample window | uint16_t | Index in a filter loop, pointer handed to a DSP routine |
| UART receive bytes | uint8_t | Head and tail pointers moving through a ring buffer |
| Sine or calibration table | const int16_t | Index into a table that lives in flash |
| DMA descriptor list | a struct | Pointer 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.
| Expression | How to read it | When you will meet it |
|---|---|---|
adc[i] | Subscript on the array | Plain index loops, the clearest form |
*(adc + i) | Move i elements forward, then dereference | What the compiler really builds underneath |
*(p + i) | Same thing through the pointer variable | Inside functions that only received a pointer |
p[i] | Subscript applied to a pointer | Readable 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.
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.
Array Name Versus Pointer Variable
Although the two behave alike inside expressions, they are different objects. This table is worth memorising before an interview.
| Property | Array name adc | Pointer variable p |
|---|---|---|
Can you write ++ on it? | No, it is not a modifiable lvalue | Yes, it simply moves forward |
sizeof result (64-bit) | 20, the whole array | 8, just the pointer |
| Occupies storage for an address? | No, the address is computed | Yes, one pointer-sized object |
&name means | Address of the whole array | Address 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
| Mistake | What you see | Fix |
|---|---|---|
| Dereferencing the end pointer | Garbage value or a hard fault on the last pass | Stop when cur == end; read only before incrementing past it |
Writing adc++ on an array | Compile error: not an lvalue | Copy it into a pointer first: int *p = adc; |
Using sizeof on a pointer parameter | Loop runs 2 times instead of 5 | Pass the element count explicitly |
| Subtracting pointers and expecting bytes | Result is 4 where you expected 16 | Cast to char * or multiply by sizeof |
Printing pointers with %d or %u | Truncated or wrong value on 64-bit | Use %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 andstart/endpointers when you are scanning. - Use
conston pointers that only read, such as thesum_samplesparameters 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
consttable 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)andp[i]reach the same element; the address isbase + i * sizeof(element).- An array name is not a pointer variable: it cannot be incremented and
sizeofsees 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.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments