Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Array Address Arithmetic in C
Learn how adding an integer to an array name lands on the right element, why the step size scales automatically, and how to count elements with pointer subtraction. Part of our free C tutorial for embedded systems.
Topics in This Lecture
In the previous lecture you saw that array elements sit side by side and that the address of element i is the base address plus i times the element size. This lecture turns that rule into C syntax. The key idea of pointer arithmetic on arrays is that arr + i already includes the multiplication, so you never scale by hand. It is one of the most useful tricks in embedded C programming, and it is a core part of this free C programming course.
All outputs below were produced by compiling the programs with GCC on 64-bit Linux, so you can reproduce them exactly on your own system in this free embedded C course.
What You Will Learn
- What the expression
arr + imeans and why it equals&arr[i] - How the compiler scales the step by the element size
- How subtracting two pointers gives an element count, not a byte count
- What a one-past-the-end pointer is and why loops use it
- The cast mistake that moves a pointer by the wrong number of bytes
- Where this arithmetic appears in real embedded code
Prerequisites
You should have finished the array memory layout lecture, and you should know how to declare a pointer and what incrementing a pointer does. Basic familiarity with stdint.h types is helpful because the examples use uint32_t and uint8_t.
Pointer Arithmetic on Arrays: What arr + i Means
When an array name appears in an expression, it converts to a pointer to its first element. If you then add an integer i to that pointer, C moves the pointer forward by i elements, not i bytes. The result points to element number i.
arr + i is the same address as &arr[i]
Say it out loud in plain words: “start at the first element, skip i elements”. The compiler knows the element type, so it multiplies i by sizeof(arr[0]) for you. That automatic scaling is the heart of pointer arithmetic and the reason a pointer needs a type at all.
How the compiler evaluates arr + 2 for a 4-byte element type
Scaling by element type
The distance moved by + 1 depends entirely on the pointer’s type. This table shows the byte step for common embedded types on typical targets.
| Element type | Size in bytes | arr + 1 moves | arr + 3 moves |
|---|---|---|---|
uint8_t | 1 | 1 byte | 3 bytes |
uint16_t | 2 | 2 bytes | 6 bytes |
uint32_t | 4 | 4 bytes | 12 bytes |
double | 8 | 8 bytes | 24 bytes |
Example 1: Proving arr + i Equals &arr[i]
This program models a small block of four 32-bit GPIO-style registers. For each index it checks that the pointer arithmetic form matches the address-of form and reports the byte step.
#include <stdio.h>
#include <stdint.h>
int main(void)
{
uint32_t gpio[4] = {0x11, 0x22, 0x33, 0x44};
for (size_t i = 0; i < 4; i++) {
uint32_t *p = gpio + i;
printf("gpio + %zu : same as &gpio[%zu]? %s, byte step = %td\n",
i, i, (p == &gpio[i]) ? "yes" : "no",
(const char *)p - (const char *)gpio);
}
return 0;
}
Build and run
gcc -Wall -Wextra -std=c11 -o l17_plus l17_plus.c
./l17_plus
Expected output
gpio + 0 : same as &gpio[0]? yes, byte step = 0
gpio + 1 : same as &gpio[1]? yes, byte step = 4
gpio + 2 : same as &gpio[2]? yes, byte step = 8
gpio + 3 : same as &gpio[3]? yes, byte step = 12
Every comparison is true. The byte step grows by 4 each time, which confirms the automatic scaling for a 32-bit type. We cast to const char * only for the printout, because a character pointer measures in single bytes.
Pointer Subtraction Counts Elements
Adding an integer to a pointer gives another pointer. Subtracting two pointers into the same array goes the other way and gives the number of elements between them. The result type is ptrdiff_t, which is declared in stddef.h and printed with %td.
Just as addition scales up, subtraction scales down: the byte difference is divided by the element size. That is why subtracting two uint32_t pointers that are 20 bytes apart gives 5, not 20.
Pointer difference divides the byte distance by the element size
Example 2: Pointer Difference and the End Pointer
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
int main(void)
{
uint32_t buf[8] = {0};
uint32_t *first = buf;
uint32_t *last = &buf[5];
ptrdiff_t elems = last - first;
ptrdiff_t bytes = (const char *)last - (const char *)first;
printf("elements between first and last : %td\n", elems);
printf("bytes between first and last : %td\n", bytes);
uint32_t *end = buf + 8;
size_t visited = 0;
for (uint32_t *p = buf; p != end; p++)
visited++;
printf("loop with end pointer visited : %zu slots\n", visited);
return 0;
}
gcc -Wall -Wextra -std=c11 -o l17_diff l17_diff.c
./l17_diff
elements between first and last : 5
bytes between first and last : 20
loop with end pointer visited : 8 slots
The first two lines show the same distance in two units. The third part introduces a pattern you will use constantly: compute an end pointer once, then loop until the walking pointer equals it.
The One-Past-the-End Pointer
An array with 8 elements has valid indices 0 to 7. Yet the expression buf + 8 in the example above is legal. C allows you to form, and compare, a pointer to the position just after the last element. This is called the one-past-the-end pointer.
Form it, compare it, but never dereference it. Writing *(buf + 8) reads memory that does not belong to the array, which is undefined behaviour. Going even further, such as buf + 9, is also undefined, even if you never use the result.
The reason this special position exists is practical. A loop that runs from buf until p != end needs an end value that is different from every valid element address. The slot just past the last element is the natural choice, and the standard guarantees it exists.
| Expression (8-element array) | Allowed? | Notes |
|---|---|---|
buf + 0 to buf + 7 | Yes | Points to a real element |
buf + 8 | Yes, do not dereference | One-past-the-end, fine for comparison |
buf + 9 | No | Undefined behaviour |
buf - 1 | No | Before the start, undefined behaviour |
*(buf + 8) | No | Out-of-bounds read |
A Cast That Changes the Step Size
A pointer’s type controls its step. If you cast the pointer to a different type before adding, the step changes. This is a favourite source of bugs in embedded C programming, especially when working with raw byte buffers and register images.
#include <stdio.h>
#include <stdint.h>
int main(void)
{
uint32_t regs[4] = {0};
uint32_t *typed = regs + 1;
uint8_t *raw = (uint8_t *)regs + 1;
printf("regs + 1 moves %td bytes\n",
(const char *)typed - (const char *)regs);
printf("(uint8_t *)regs + 1 moves %td bytes\n",
(const char *)raw - (const char *)regs);
return 0;
}
gcc -Wall -Wextra -std=c11 -o l17_cast l17_cast.c
./l17_cast
regs + 1 moves 4 bytes
(uint8_t *)regs + 1 moves 1 bytes
The first expression adds one element of type uint32_t, which is four bytes. The second converts the pointer to uint8_t * first, so the “one element” is a single byte. Both are legal; only one is probably what you meant. The cast changes the meaning of + 1, so think about the type at the moment you add.
Where This Is Used in Embedded Systems
- Register banks: a base pointer to a peripheral plus an index selects the n-th channel register.
- Frame buffers: pointer to the start of a row is the buffer pointer plus
row * width, scaled automatically. - Protocol parsing: a byte pointer advances by header length to reach the payload; here the element type is deliberately one byte.
- Bounded loops: iterating from a start pointer to an end pointer avoids keeping a separate index variable.
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
Multiplying by sizeof manually: arr + i * sizeof(*arr) | Pointer jumps far beyond the array | Write arr + i only; scaling is automatic |
| Subtracting pointers from different arrays | Undefined behaviour, meaningless value | Only subtract pointers into the same array |
Dereferencing arr + n | Out-of-bounds read | Use it only as an end marker |
Expecting a byte count from p2 - p1 | Value is too small by the element size | Cast to char * for bytes |
Printing ptrdiff_t with %d | Warning or wrong value on 64-bit | Use %td |
Best Practices, Performance and Security
Best practices
- Let the compiler scale; never write the element size into pointer arithmetic.
- Keep every computed pointer between the first element and one past the last.
- Use
conston pointers that only read data. - Use a named
endpointer to make loop conditions readable.
Performance: arr + i compiles to a single address calculation, often folded into the load instruction itself, so there is no extra cost compared with indexing.
Security: pointer arithmetic is not bounds-checked. A miscalculated offset can read or overwrite unrelated memory. Validate lengths that come from outside your program, such as packet sizes, before using them in arithmetic.
Summary and Key Takeaways
arr + iis the address of elementi, identical to&arr[i].- The compiler multiplies
iby the element size automatically. - Subtracting two pointers into one array returns an element count of type
ptrdiff_t. - A one-past-the-end pointer is valid for comparison but must never be dereferenced.
- Casting a pointer before adding changes the step size.
Conclusion
You can now move to any element of an array using only the array name and an integer, and you understand why this works for every data type. The next lecture takes the final step: dereferencing these addresses with * to read and write element values, and showing that arr[i] and *(arr + i) are two spellings of the same operation. Run the three programs above, then try changing the element type to uint16_t and predict the new step sizes before you run them. That habit of predicting first is the fastest way to master pointer arithmetic on arrays.
Frequently Asked Questions
Why does arr + 1 move by the element size and not one byte?
Because the pointer has a type. C multiplies the integer by the size of the pointed-to type, so a uint32_t pointer advances four bytes per step.
Is arr + i the same as &arr[i]?
Yes. They produce the same address and the same pointer type. The C standard defines the subscript operator in terms of this arithmetic.
What does subtracting two pointers return?
The number of elements between them, as a ptrdiff_t. Both pointers must point into the same array, or one past its end.
Can I dereference the one-past-the-end pointer?
No. You may compute it and compare against it, but reading or writing through it is undefined behaviour.
How do I move a pointer by a number of bytes?
Cast it to a byte-sized pointer type such as uint8_t * or char * before adding. Then each unit of addition is one byte.
Is pointer arithmetic on arrays used in embedded C programming?
Very often. It appears in register block access, buffer walking, protocol parsing and DMA setup, which makes it a key skill in any free embedded C course.
Keep Going With the Free C Tutorial
Next up: dereferencing array addresses and the equivalence of subscript and pointer notation. Continue your free C programming course for embedded systems.
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments