Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Pointer Notation for Arrays
Read and write array elements with the dereference operator, see why arr[i] and *(arr + i) are the same thing, and test the old claim that pointer notation is faster. A practical lesson in our free C tutorial for embedded systems.
Topics in This Lecture
So far in this free C programming course you have learned where array elements live and how to compute their addresses. Now we add the final ingredient: the dereference operator. Combining the two gives pointer notation for arrays, the form *(arr + i), which the compiler treats as exactly equivalent to arr[i]. Understanding this equivalence is a milestone in embedded C programming, because driver code, firmware libraries and the Linux kernel freely mix the two styles.
We will also test, with real compiler output, a statement found in many older books: that pointer notation is faster than subscripts. The answer may surprise you.
What You Will Learn
- How
*arrand*(arr + i)read element values - Why
arr[i]is defined as*(arr + i) - The curious but legal form
i[arr]and why you should never write it - How to write to array elements using pointer notation
- How to walk an array with a moving pointer and an end pointer
- What the compiler really generates for both styles
- The precedence trap in
*arr + 1
Prerequisites
You need the two previous lectures: array memory layout and address arithmetic with arr + i. You should also remember that the unary * operator follows a pointer to the value it points at.
Dereferencing an Array Name
An address on its own tells you where something lives. Applying * to that address gives you the object stored there. Because an array name converts to the address of its first element, the expression *arr is the value of the first element, the same as arr[0].
For the element at position i, first compute the address with arr + i, then dereference it. The parentheses matter, and we will see why in a moment.
From array name to element value
The following table maps the two notations side by side for a five-element array called temp.
| Pointer notation | Subscript notation | Meaning |
|---|---|---|
temp | &temp[0] | Address of element 0 |
temp + i | &temp[i] | Address of element i |
*temp | temp[0] | Value of element 0 |
*(temp + i) | temp[i] | Value of element i |
Why arr[i] Is the Same as *(arr + i)
This is not a coincidence or an optimisation. The C language defines the subscript operator in exactly these terms: the expression E1[E2] is by definition *((E1) + (E2)). When you write arr[3], the compiler rewrites it as *(arr + 3) in its own mind and carries on. Subscript notation is simply friendlier syntax laid on top of pointer arithmetic.
A direct consequence is that anything which works for one form works for the other, including assignment, compound assignment, increment and passing the element’s address to a function.
Example 1: Four Ways to Read the Same Element
This program prints each element using the four spellings side by side, then modifies two elements through pointer notation.
#include <stdio.h>
int main(void)
{
int temp[5] = {18, 21, 24, 27, 30};
for (int i = 0; i < 5; i++) {
printf("i=%d temp[i]=%d *(temp+i)=%d *(i+temp)=%d i[temp]=%d\n",
i, temp[i], *(temp + i), *(i + temp), i[temp]);
}
*temp = 15;
*(temp + 4) += 5;
printf("after writes: first=%d last=%d\n", temp[0], temp[4]);
return 0;
}
Build and run
gcc -Wall -Wextra -std=c11 -o l18_equiv l18_equiv.c
./l18_equiv
Expected output
i=0 temp[i]=18 *(temp+i)=18 *(i+temp)=18 i[temp]=18
i=1 temp[i]=21 *(temp+i)=21 *(i+temp)=21 i[temp]=21
i=2 temp[i]=24 *(temp+i)=24 *(i+temp)=24 i[temp]=24
i=3 temp[i]=27 *(temp+i)=27 *(i+temp)=27 i[temp]=27
i=4 temp[i]=30 *(temp+i)=30 *(i+temp)=30 i[temp]=30
after writes: first=15 last=35
Reading the result
- All four columns match on every line, confirming the equivalence.
*temp = 15replaces the first element, sotemp[0]becomes 15.*(temp + 4) += 5adds 5 to the last element, turning 30 into 35. Pointer notation works on the left side of assignments just like subscripts do.
The Strange Form i[arr]
Addition is commutative, so arr + i and i + arr are the same address. Since arr[i] means *(arr + i), it follows that *(i + arr) is also valid, and that can be spelled i[arr]. The fifth column in our output proves it compiles and works.
Know it, never use it. i[arr] is a curiosity that shows you how the language is defined. It is a favourite of interview questions and obfuscation contests, but in real firmware it only confuses readers and code reviewers. Always write arr[i].
Precedence Trap: *arr + 1 vs *(arr + 1)
The unary * binds tighter than binary +. So *temp + 1 means “take the first element, then add one to the value”, while *(temp + 1) means “move to the second element, then read it”. With our original array, the first gives 19 and the second gives 21. Forgetting the parentheses is one of the most common beginner errors in pointer notation for arrays.
| Expression | Steps | Result for {18, 21, 24, 27, 30} |
|---|---|---|
*temp + 1 | Read element 0, then add 1 | 19 |
*(temp + 1) | Move to element 1, then read | 21 |
*(temp + 4) | Move to element 4, then read | 30 |
Walking an Array With a Moving Pointer
Once you accept that elements are reached through addresses, a second looping style becomes natural: keep a pointer, read through it, and advance it. Instead of an index that is converted to an address on every iteration, you carry the address itself.
Index loop vs pointer-walk loop
Example 2: Index Style vs Pointer Style
Two functions add up the same six readings. One uses subscripts, the other uses a walking pointer with an end marker.
#include <stdio.h>
static int sum_index(const int *a, int n)
{
int total = 0;
for (int i = 0; i < n; i++)
total += a[i];
return total;
}
static int sum_walk(const int *a, int n)
{
int total = 0;
for (const int *p = a, *end = a + n; p != end; p++)
total += *p;
return total;
}
int main(void)
{
int load[6] = {4, 8, 15, 16, 23, 42};
printf("index style: %d\n", sum_index(load, 6));
printf("walk style: %d\n", sum_walk(load, 6));
return 0;
}
gcc -Wall -Wextra -std=c11 -o l18_walk l18_walk.c
./l18_walk
index style: 108
walk style: 108
Both give 108. Parameters are declared const int * because the functions only read the data, which documents intent and lets the compiler catch accidental writes.
Is Pointer Notation Faster Than Subscripts?
Many older textbooks state that accessing elements through pointers is faster because the compiler converts subscripts to pointer form anyway. The reasoning sounds neat, but the conclusion does not hold on modern compilers. Both versions describe the same address calculation, and an optimising compiler is free to generate whatever instructions it likes for either.
We checked this with GCC 13 on x86-64 at -O2, preventing inlining so each function stays separate. Looking at the assembly, the work done in the loop body of both functions is the same four steps: load and add the element, advance the pointer by 4 bytes, compare with the end, and branch back.
addl (%rdi), %eax
addq $4, %rdi
cmpq %rdx, %rdi
jne .loop
To repeat the experiment yourself, mark both functions with GCC’s __attribute__((noinline)), then run:
gcc -O2 -S -o sum.s l18_walk.c
grep -n "addl" sum.s
The only differences between the two functions were the handling of the empty-array case before the loop. The lesson: choose the style that reads best, and measure on your actual target if performance matters. On very small microcontrollers with weak compilers the story can differ, but never assume; look at the generated code.
Use Cases in Embedded Firmware
- Checksum and CRC loops: a byte pointer walks a buffer until it reaches the end pointer.
- Driver code: kernel and HAL code often steps through descriptor arrays using pointers.
- Parsers: a cursor pointer advances as fields are consumed from a received frame.
- Table lookups:
*(table + index)andtable[index]are interchangeable; use the latter for clarity.
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
*arr + i instead of *(arr + i) | Always reads element 0 and adds i | Add parentheses around the address expression |
Loop with p <= end | Reads one element past the array | Use p != end or p < end |
Writing through a const pointer | Compile error | Remove const only if writing is intended |
Using i[arr] in real code | Reviewers confused | Write arr[i] |
| Modifying the array while a pointer walks it | Unexpected results | Keep reading and writing roles clear |
Best Practices, Performance and Security
Best practices
- Prefer
arr[i]when you think in positions, and a walking pointer when you think in sweeps. - Always parenthesise
*(arr + i). - Mark read-only data with
const. - Pass the element count along with the pointer, because the callee cannot know it.
Performance: do not rewrite working subscript code into pointer style hoping for speed. Measure, or read the assembly.
Security: a mistake in the end condition silently reads or overwrites neighbouring memory. Treat any length supplied by a remote peer as untrusted and check it against the buffer size before looping.
Summary and Key Takeaways
*(arr + i)is the value of elementi, andarr[i]is defined as exactly that.*arris the first element;*arr + 1and*(arr + 1)are very different.i[arr]works but should never appear in real code.- Pointer notation can be used for reading and writing.
- Index loops and pointer-walk loops can compile to the same machine instructions.
Conclusion
You now know that subscripting is pointer arithmetic plus dereferencing in a friendlier outfit. That knowledge helps you read any C source, from small firmware to operating system kernels, without being thrown by unfamiliar notation. The final lecture of this group completes the picture by comparing an array name with a pointer variable: they look alike but they are not the same, and the differences matter. Re-run the programs here, change the element values, and predict the output of each column before you run.
Frequently Asked Questions
Is arr[i] really identical to *(arr + i)?
Yes. The C standard defines the subscript operator as the dereference of the sum of the pointer and the index.
Why does i[arr] compile?
Because addition is commutative, i + arr equals arr + i, so *(i + arr) is valid and can be written i[arr]. It is legal but poor style.
Is pointer notation faster than array subscripts in C?
Not on modern optimising compilers. Both forms describe the same address calculation and typically produce the same loop code. Check the assembly or benchmark on your target.
What is the difference between *arr + 1 and *(arr + 1)?
The first reads element 0 and adds 1 to the value. The second moves to element 1 and reads it, because the parentheses force the address calculation first.
Can I assign through pointer notation?
Yes. Statements such as *(arr + 2) = 9 and *arr += 3 modify array elements exactly like subscript assignments.
Do I need this for embedded C programming?
Yes. Firmware, HAL libraries and kernel drivers mix both styles, so reading and writing pointer notation fluently is essential and is a core topic of this free embedded C course.
Finish the Array and Pointer Story
The next lecture compares an array name with a pointer variable, and includes a quiz and interview questions. Keep going with our free C programming course.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments