C Pointer Arithmetic

Pointer arithmetic is the part of C that lets a pointer move from one array element to another. It matters because arrays, strings, buffers, and many low-level algorithms are built around the idea of walking through contiguous memory one element at a time.

The important surprise is that p + 1 does not usually mean one byte forward. It means one object of the pointer’s pointed-to type forward, which is why pointer arithmetic works naturally with arrays but becomes dangerous outside valid bounds.

Overview: How Pointer Arithmetic Works

A pointer stores an address, but C does not treat pointer arithmetic as plain integer arithmetic. If p has type int *, then p + 1 points to the next int position. If p has type double *, then p + 1 points to the next double position. The compiler scales the movement by sizeof *p.

This scaling is what makes arrays and pointers closely related. An array stores its elements contiguously: element 0, then element 1, then element 2, and so on. In most expressions, an array name is converted to a pointer to its first element. Therefore, if int values[4] exists, values often behaves like &values[0], and values + 2 points at values[2].

The expression a[i] is defined in terms of pointer arithmetic: it means *(a + i). This equivalence is useful for understanding how indexing works internally, but it does not mean pointer arithmetic is always clearer than indexing. In many programs, indexes are easier to read. Pointer arithmetic shines when you are processing a range represented by a beginning pointer and an end pointer.

C allows pointer addition and subtraction only in a narrow, important context: within the same array object, including a special one-past-the-end pointer. The one-past pointer points just after the last element. It is legal to form and compare, but not legal to dereference. For an array of count elements, array + count is the usual end marker for loops.

You may subtract two pointers only when both point into the same array object, or one points one past that same array. The result has type ptrdiff_t, from <stddef.h>, and gives the distance in elements, not bytes. Comparing unrelated pointers, subtracting unrelated pointers, or walking outside an array produces undefined behavior.

Syntax

pointer + integer
pointer - integer
pointer++
pointer--
pointer2 - pointer1
*pointer
*(pointer + index)
Expression Meaning
p + n A pointer n elements after p, scaled by the pointed-to type.
p - n A pointer n elements before p.
p++ Move p to the next element position after using its old value.
++p Move p first, then use the new pointer value.
last - first The number of elements between two pointers in the same array, as ptrdiff_t.
*(p + i) Dereference the element reached by moving i positions from p.
array + count The one-past-the-end pointer, valid for comparison but not dereference.

Pointer arithmetic works on object pointers such as int *, double *, and char *. It does not work on void * in standard C because void has no size. When you need byte-by-byte movement through raw storage, use a character pointer such as unsigned char *.

Examples

Move Through an Array

#include <stdio.h>

int main(void)
{
    int values[] = {10, 20, 30, 40};
    int *p = values;

    printf("First: %d
", *p);
    p++;
    printf("Second: %d
", *p);
    printf("Fourth: %d
", *(values + 3));

    return 0;
}

Output:

First: 10
Second: 20
Fourth: 40

The name values converts to a pointer to the first element. After p++, p points to the second int, not merely to the next byte. The expression *(values + 3) reaches the same object as values[3].

Sum a Range With Begin and End Pointers

#include <stdio.h>
#include <stddef.h>

int main(void)
{
    int readings[] = {6, 9, 3, 8, 4};
    size_t count = sizeof readings / sizeof readings[0];
    int *current = readings;
    int *end = readings + count;
    int total = 0;

    while (current < end) {
        total += *current;
        current++;
    }

    printf("Total: %d
", total);
    printf("Count: %zu
", count);

    return 0;
}

Output:

Total: 30
Count: 5

This loop uses the common range pattern: current starts at the first element, and end is the one-past-the-end pointer. The loop dereferences only while current < end, so it never reads the one-past pointer.

Measure Distance Between Pointers

#include <stdio.h>
#include <stddef.h>

int main(void)
{
    char word[] = "pointer";
    char *start = word;
    char *letter = word + 3;
    char *end = word + 7;
    ptrdiff_t offset = letter - start;
    ptrdiff_t length = end - start;

    printf("Letter: %c
", *letter);
    printf("Offset: %td
", offset);
    printf("Length without null: %td
", length);

    return 0;
}

Output:

Letter: n
Offset: 3
Length without null: 7

Pointer subtraction reports element distance. Because these are char * pointers, the distance is also a byte count, but that is a special property of character arrays. The string literal initializer creates eight array elements: seven visible letters plus the terminating null character. This example sets end to the position after the visible letters.

Use Pointer Arithmetic in a Function

#include <stdio.h>
#include <stddef.h>

void add_to_each(int *values, size_t count, int amount)
{
    int *end = values + count;

    for (int *p = values; p < end; p++) {
        *p += amount;
    }
}

void print_values(const int *values, size_t count)
{
    const int *end = values + count;

    for (const int *p = values; p < end; p++) {
        printf("%d", *p);
        if (p + 1 < end) {
            printf(", ");
        }
    }
    printf("
");
}

int main(void)
{
    int scores[] = {81, 87, 90, 76};
    size_t count = sizeof scores / sizeof scores[0];

    add_to_each(scores, count, 5);
    print_values(scores, count);

    return 0;
}

Output:

86, 92, 95, 81

When an array is passed to a function, the parameter receives a pointer to the first element. The function therefore also needs count. Both functions build an end pointer and walk from the beginning to that end position.

How It Works Step by Step

  1. The array declaration creates a contiguous sequence of element objects.
  2. In an expression such as values, the array usually converts to a pointer to its first element.
  3. When the program evaluates values + 3, the compiler computes an address three int elements after the beginning, not three raw bytes after it.
  4. The dereference operator in *(values + 3) accesses the object at that computed element position.
  5. A loop such as p++ repeats this scaled movement one element at a time.
  6. The pointer values + count is allowed as an end marker, but dereferencing it would be out of bounds.
  7. Subtracting two pointers from the same array converts the address difference back into an element count.

The compiler can generate very efficient machine code for these operations because the element size is known from the pointer type. That same type information is why casting pointers casually can break programs: a different pointer type changes how arithmetic and dereferencing are interpreted.

Common Mistakes

Thinking p + 1 Means One Byte

If p is an int *, p + 1 moves by sizeof(int) bytes internally. Correct code thinks in elements: use int *next = p + 1; for the next integer. If you truly need byte movement, use unsigned char *bytes and move that pointer.

Dereferencing the One-Past Pointer

int *end = values + count; is a useful loop boundary, but *end is invalid. The correct loop condition is p < end, followed by *p inside the loop. A condition such as p <= end goes one step too far.

Subtracting Unrelated Pointers

Pointer subtraction is defined only for positions in the same array object. Do not subtract pointers into two different arrays, two separate variables, or two separately allocated blocks. If you need a distance, keep the pointers tied to the same buffer and pass both the beginning and end of that buffer together.

Losing the Start of a Buffer

If you increment the only pointer you have, you may lose the original address needed for later cleanup or for computing an offset. Prefer separate names such as begin, current, and end when a function needs both the original range and the moving position.

Best Practices

  • Use pointer arithmetic mainly for arrays, strings, and explicit begin/end ranges.
  • Form an end pointer with array + count, compare against it, and never dereference it.
  • Use ptrdiff_t and the %td format specifier for pointer differences.
  • Use size_t for element counts and indexes.
  • Keep pointer arithmetic within one array object, including the legal one-past position.
  • Prefer array indexing when an index expresses the idea more clearly.
  • Use const pointers for traversal functions that should read but not modify elements.
  • Avoid converting pointers to integers for address math; standard pointer arithmetic already handles element scaling.
  • Use character pointers for byte-level inspection, not void * arithmetic.

Practice Exercises

  1. Write a program that stores six integers and uses int *begin and int *end to print them in order.
  2. Write a function size_t count_even(const int *values, size_t count) that uses pointer arithmetic to count even numbers without indexing.
  3. Create a character array containing a short word. Use two pointers to print the characters in reverse order without modifying the array.

Summary

  • Pointer arithmetic moves in elements, not raw bytes, based on the pointed-to type.
  • a[i] is equivalent to *(a + i), which explains how array indexing maps to memory.
  • The one-past-the-end pointer is legal for comparison but illegal to dereference.
  • Pointer subtraction gives an element distance as ptrdiff_t when both pointers belong to the same array.
  • Pointer arithmetic outside a valid array range causes undefined behavior.
  • Begin/end pointer ranges are a common, efficient way to process arrays and strings in C.