What are Array Address Arithmetic in C- Embedded C Programming Training In Hyderabad

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 17

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

pointer arithmetic on arrays arr + i pointer scaling pointer subtraction one past the end free embedded C course

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 + i means 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

arr→base address B
arr + 2→B + 2 × 4→B + 8→address of arr[2]

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 typeSize in bytesarr + 1 movesarr + 3 moves
uint8_t11 byte3 bytes
uint16_t22 bytes6 bytes
uint32_t44 bytes12 bytes
double88 bytes24 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

last = &buf[5]−first = buf=20 bytes ÷ 4=5 elements

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 + 7YesPoints to a real element
buf + 8Yes, do not dereferenceOne-past-the-end, fine for comparison
buf + 9NoUndefined behaviour
buf - 1NoBefore the start, undefined behaviour
*(buf + 8)NoOut-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

MistakeSymptomFix
Multiplying by sizeof manually: arr + i * sizeof(*arr)Pointer jumps far beyond the arrayWrite arr + i only; scaling is automatic
Subtracting pointers from different arraysUndefined behaviour, meaningless valueOnly subtract pointers into the same array
Dereferencing arr + nOut-of-bounds readUse it only as an end marker
Expecting a byte count from p2 - p1Value is too small by the element sizeCast to char * for bytes
Printing ptrdiff_t with %dWarning or wrong value on 64-bitUse %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 const on pointers that only read data.
  • Use a named end pointer 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 + i is the address of element i, identical to &arr[i].
  • The compiler multiplies i by 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

Leave a Reply

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