Skip to content
academia.sh

Lesson 02 / 15

Pointer Arithmetic

Why incrementing a pointer, decrementing it, and subtracting two pointers operates in elements rather than bytes; why an arithmetic that runs past the bounds is never run.

Published Updated

In this lesson

  • Measures how incrementing and decrementing a pointer advances according to its type's size
  • Computes the difference between two pointers in element units and measures their order with comparison
  • Explains why an arithmetic that runs past an array's bounds is never run
Contents

The previous lesson’s local array row gave a last - first difference of 3: in a four-element array, there are three steps between the first and last elements. But that difference is not in bytes — the actual memory distance between last and first is a multiple of however many bytes the array’s element type takes up. This lesson measures where that multiplier comes from, and what it means to increment and decrement a pointer. The class is still language: moving between an array’s elements requires no allocation or free call at all.

Increment and Decrement: Advancing by Type Size

// increment.c — incrementing a pointer advances by its type's size
#include <stdio.h>

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

    printf("%s %d\n", "first element", *p);
    p++;
    printf("%s %d\n", "element after one increment", *p);
    p += 2;
    printf("%s %d\n", "element after three increments", *p);
    p--;
    printf("%s %d\n", "element after one decrement", *p);

    double pair[3] = {1.5, 2.5, 3.5};
    double *d = pair;
    d++;
    printf("%s %.1f\n", "element after one increment on a double array", *d);
    return 0;
}
first element 10
element after one increment 20
element after three increments 40
element after one decrement 30
element after one increment on a double array 2.5

PO5. p++ moves the pointer to the next int — the step written is always one element, never one byte. p += 2 skips two elements at once: from index 1 to index 3. p-- steps back by one element. The same p++ spelling was used on the pair array of double values, and it again advanced by exactly one element — even though int and double have different sizes, the increment operator means “the next element” for both. The size itself (sizeof) was never printed anywhere; the measurement was made only through the pointed-to value.

The Difference and Order of Two Pointers

// diff_and_order.c — the difference between two pointers is in elements, their order in comparison
#include <stdio.h>

int main(void) {
    int array[6] = {1, 2, 3, 4, 5, 6};
    int *first = &array[0];
    int *middle = &array[3];
    int *last = &array[5];

    printf("%s %d\n", "middle minus first", (int)(middle - first));
    printf("%s %d\n", "last minus middle", (int)(last - middle));
    printf("%s %d\n", "does first come before middle", (int)(first < middle));
    printf("%s %d\n", "does last come before middle", (int)(last < middle));

    int *walker = first;
    int steps = 0;
    while (walker < last) {
        walker++;
        steps++;
    }
    printf("%s %d\n", "steps taken from first to last", steps);
    return 0;
}
middle minus first 3
last minus middle 2
does first come before middle 1
does last come before middle 0
steps taken from first to last 5

PO6. middle - first gives 3: there is a gap of three elements between array[3] and array[0]. The result of this subtraction is always the element count, never the byte difference between two addresses — the expression middle - first prints 3, and the addresses themselves are never seen. The comparison first < middle gives 1 because first sits before middle in the array; this ordering comes from the < operator being defined for two pointers within the same array. The loop built with the condition walker < last advances exactly 5 steps from first to last — which matches the last - first difference exactly.

Arithmetic on a char Array: When the Element Step Equals the Byte Step

The previous two examples used int and double arrays; in both, a single increment advanced by more than one byte, but that advance was never measured directly — only the pointed-to value was read. The char type creates a special case here: sizeof(char) is always 1, so incrementing a char * covers a step of one element and a step of one byte at the same time. This case comes up often when moving through a string’s characters.

// character_walk.c — one increment on a char pointer moves to the next character
#include <stdio.h>

int main(void) {
    char string[] = "abcd";
    char *letter = string;

    printf("%s %c\n", "first character", *letter);
    letter++;
    printf("%s %c\n", "character after one increment", *letter);

    char *last_letter = &string[3];
    printf("%s %d\n", "last letter minus first letter", (int)(last_letter - string));
    printf("%s %c\n", "value of the last character", *last_letter);
    return 0;
}
first character a
character after one increment b
last letter minus first letter 3
value of the last character d

PO7. The call letter++ moves from string[0] to string[1]; the value read changes from 'a' to 'b'. The difference last_letter - string gives 3 — string[3] is three elements from the start of the array. This difference follows the same rule as the middle - first example in the int array: the result is in elements. That the difference also happens to equal a byte distance for char is a direct consequence of sizeof(char) being 1 — not an exception to the rule, but the shape the rule itself takes for char.

Row Stepping in a Two-Dimensional Array

The three examples so far used one-dimensional arrays; the increment always advanced by one element. In a two-dimensional array this rule does not change, but the definition of “element” does: when a row pointer (int (*)[4]) is defined over int matrix[3][4], an increment skips not a single int but an entire row (4 ints).

// row_step.c — a row pointer in a two-dimensional array counts a whole row as one step
#include <stdio.h>

int main(void) {
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    int (*row)[4] = matrix;

    printf("%s %d\n", "first element of the first row", row[0][0]);
    row++;
    printf("%s %d\n", "first element of the row after one increment", row[0][0]);
    printf("%s %d\n", "third element of the same row", row[0][2]);

    int *flat = &matrix[0][0];
    int *second_row_start = &matrix[1][0];
    printf("%s %d\n", "second row start minus first element", (int)(second_row_start - flat));
    return 0;
}
first element of the first row 1
first element of the row after one increment 5
third element of the same row 7
second row start minus first element 4

PO8. The row variable has type int (*)[4] — it carries the type “address of an array of four ints,” not a plain int *. When row++ is called, the pointer moves to the next row: the value of row[0][0] jumps from 1 to 5, that is, to the start of matrix’s second row. This is the same rule as p++ advancing by one int in increment.c, seen on a different type: the step is always the size of the pointed-to type, and here that type is not a single int but a row of four ints. The difference between a plain int * (flat) and the start of the second row comes out to 4 — which confirms why int matrix[3][4] decays to int (*)[4] rather than int ** when passed to a function: the row size (4) travels as part of the type, otherwise row++ could not advance to the correct element.

The Same Rule on a Dynamically Allocated Array

The three examples so far ran on local (language class) arrays. The same arithmetic rules hold exactly on a dynamically allocated block as well — advancing by type size does not require the pointer to know where the array came from.

// dynamic_increment.c — pointer arithmetic on a dynamically allocated block follows the same rule
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int length = 4;
    int *array = malloc((size_t)length * sizeof *array);
    if (array == NULL) {
        printf("%s\n", "allocation failed");
        return 1;
    }
    for (int i = 0; i < length; i++) {
        array[i] = (i + 1) * 10;
    }

    int *p = array;
    printf("%s %d\n", "first element", *p);
    p += 2;
    printf("%s %d\n", "element after two increments", *p);

    int *last = array + length - 1;
    printf("%s %d\n", "last minus first", (int)(last - array));

    free(array);
    array = NULL;
    return 0;
}
first element 10
element after two increments 30
last minus first 3

PO9. array is a block allocated with malloc, but the call p += 2 produces exactly the same result as the local array in increment.c: it skips two elements, from 10 to 30. The difference last - array is 3 — the same rule as the middle - first example in diff_and_order.c. The difference is not in the arithmetic but in ownership: this block was requested with malloc, so its class here is caller — main allocates array, main uses it, and the obligation to free it stays with main as well. The arithmetic itself is entirely independent of that ownership fact: whether the block a pointer points to is a local array in the language class or a dynamic block in the caller class, how far p++ advances depends only on the type, never on where the memory came from.

The Equivalence Between Indexing and Arithmetic

So far, p[i] notation and p + i notation have each been used separately, but the relationship between them was never measured. In C, square-bracket indexing is a shorthand defined in terms of arithmetic: the expression p[i] means exactly *(p + i), neither a byte off nor a separate rule.

// index_equivalence.c — p[i] notation and *(p + i) notation point at the same address
#include <stdio.h>

static int index_it(int *p, int i) {
    return p[i];
}

static int add_by_dereference(int *p, int i) {
    return *(p + i);
}

int main(void) {
    int array[5] = {100, 200, 300, 400, 500};
    int *p = array;

    int index_result = index_it(p, 3);
    int arithmetic_result = add_by_dereference(p, 3);
    printf("%s %d\n", "p indexed at three", index_result);
    printf("%s %d\n", "p advanced three times and dereferenced", arithmetic_result);
    printf("%s %d\n", "do the two notations give the same value", (int)(index_result == arithmetic_result));

    int zeroth_index = index_it(p, 0);
    int zeroth_arithmetic = add_by_dereference(p, 0);
    printf("%s %d\n", "zeroth index", zeroth_index);
    printf("%s %d\n", "same as dereferenced p", (int)(zeroth_index == zeroth_arithmetic));
    return 0;
}
p indexed at three 400
p advanced three times and dereferenced 400
do the two notations give the same value 1
zeroth index 100
same as dereferenced p 1

PO10. The expressions p[3] and *(p + 3) give the same value (400), and the equality comparison comes out 1 — confirming that the two notations express exactly the same operation to the compiler. The equality between p[0] and *p is an edge case of the same rule: the zeroth index is identical to dereferencing directly, with no advance at all. This equivalence explains why all of this lesson’s p++, p += n, and last - first measurements gave results consistent with array[i] notation: indexing is a notational convenience built on top of arithmetic, not a separate mechanism. The class is still language: both p[3] and *(p + 3) point at the same local array, and the only difference between them is notation. This equivalence also explains something else: even array[i] notation is itself a form of pointer arithmetic, because array decays to the address of its first element whenever it enters a function or an operator context, and array[i] becomes exactly equivalent to *(array + i).

The Bounding Measurement

Pointer arithmetic is defined only within the same array, or one element past its end — pointing one past the array’s last element (for loop termination) is valid, but dereferencing that address, or advancing the array by two elements past it, is undefined behavior. In the diff_and_order.c example above, walker advances during the loop only as far as the address last and stops there — an increment that runs past the array’s bound is never run. This bound is not something the compiler checks: writing p += 100 compiles too, but its result is undefined, and this lesson never includes such a call. The validity of the arithmetic depends on the person writing the code knowing the array’s bounds — the type system does not carry that bound.

The same bound applies to pointer values that fall outside a statically allocated block such as array: pointing to one before the start of the array with p - 1, or going further than last + 1, falls into the same class of undefined behavior. This is exactly why the bound is defined as “one element past the end”: the standard guarantees just enough that a loop termination condition (like walker < last) can safely reach that one address, and no more.

This bound also holds between pointers belonging to two unrelated arrays: if int a[4] and int b[4] are two separately defined local arrays, &b[0] - &a[0] compiles but its result is undefined — the subtraction operator only guarantees a meaningful element count for two pointers within the same array (or one element past it); where two different arrays sit relative to each other in memory is a detail left to the compiler’s decision, and a program cannot rely on it. Both the diff_and_order.c and dynamic_increment.c examples above observe this rule indirectly: both take a difference only between two pointers within a single array or a single block, never comparing two unrelated blocks like a and b.

Summary

  • Pointer increment and decrement (p++, p--, p += n) always advance by the type’s size; the step count is in elements, never in bytes.
  • The difference between two pointers (last - first) gives the element count between them; the address itself is never printed.
  • Two pointers can be ordered with comparison operators like < and > — within the same array, that ordering matches their position order in the array.
  • Pointing one past an array’s bound is valid, but dereferencing that address is undefined behavior; taking the difference between pointers of two unrelated arrays falls into the same class — neither was run in this lesson.
  • p[i] notation is a shorthand for *(p + i); the two notations always point at the same address and return the same value.
  • The arithmetic rules do not change whether the block a pointer points to is a local array (language) or a dynamically allocated block (caller); the step depends only on the type, never on the memory’s source. This is why the memory this lesson measures is in the language class: the rule is set by the language itself, not by the memory’s owner.

Next Step

An array’s name decays on its own into the address of its first element in most contexts — the int *first = array notation already used this without dwelling on it. The next lesson, Arrays and Pointers, measures when this decay happens, how an array is passed to a function (and why without length information), and the first step into the caller class: the ownership of a dynamically allocated array.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close