Topic 71 of 100 Pointers in C Interactive Tutorial

Pointer Arithmetic in C: Moving Through Memory With Pointers

13 mins read Last updated: 22 Aug 2026 25,700 views
C Compiler 0.00%

What is Pointer Arithmetic in C? Core Concept & Direct Summary

Core Objective & Problem Statement

How do pointer addition (`ptr + n`), subtraction (`ptr - n`), and difference (`ptr1 - ptr2`) scale automatically by `sizeof(type)`?

Direct Answer Summary

Adding one to a variable normally adds one. But add one to a pointer, and something stranger happens: the address jumps forward by four bytes, or eight, or twenty-four — depending on what the pointer points at.

That's pointer arithmetic, and it's the reason C's arrays work the way they do. It's also the reason arr[i] and *(arr + i) are the same thing, why loops can be written with pointers instead of indexes, and why the compiler needs to know the pointed-to type at all.

This page covers the mechanics: what +, -, ++, -- actually do to pointers, how to subtract one pointer from another, what comparisons are legal, the one-past-the-end rule, and the operations C simply forbids. Then we'll use it to write some loops that read like the low-level C of decades past — which is still, in some contexts, the clearest way to express an algorithm.

(Suggested image: the hero graphic — an int array of four cells with pointers p, p+1, p+2, p+3 above them, and the byte offsets 0, 4, 8, 12 marked below. The "type scales the step" idea in one image.)

How Does Pointer Arithmetic in C Work? In-Depth Mechanics & Architecture

The Core Rule: Arithmetic Scales by Type Size

main.c
Run Online
int nums[4] = {
    10, 20, 30, 40
}
;
int *p = nums;
// points at nums[0]

Now:

main.c
Run Online
p → nums[0](the same address as nums)p + 1 → nums[1](4 bytes further along)p + 2 → nums[2](8 bytes further along)p + 3 → nums[3](12 bytes further along)

The pointer moves by whole elements, not by bytes. That's because the compiler knows the type:

main.c
Run Online
int *ip;
ip + 1 → 4 bytes forwardchar *cp;
cp + 1 → 1 byte forwarddouble *dp;
dp + 1 → 8 bytes forward

The actual address arithmetic is:new_address = old_address + (n × sizeof(*pointer)).

This is why pointer types matter, even though all pointers are the same size themselves. The type is what tells the compiler how big a "step" is. char * walks byte by byte; int * walks four at a time; a struct pointer strides by the whole struct's size.

(Suggested image: three parallel arrays in the diagram — char pointer stepping 1 byte, int stepping 4, double stepping 8 — the type-scaling rule made visual)


The Five Legal Operations

C permits exactly these, and nothing else:

OperationExampleMeaning
Incrementp++, ++pMove to the next element
Decrementp--, --pMove to the previous element
Add an integerp + 3Move forward 3 elements
Subtract an integerp - 2Move backward 2 elements
Subtract two pointersp2 - p1Distance between them (in elements)

And comparisons (<, >, ==, !=) between pointers into the same array are also legal — we'll cover those separately since they behave differently from the arithmetic itself.

What is NOT legal:

main.c
Run Online
p1 + p2 // ✗ adding two pointers (what would it even mean?)p * 2            // ✗ multiplying a pointerp / 2            // ✗ dividing a pointer

Adding addresses is meaningless — you'd be adding two locations together. So C forbids it, and the compiler will tell you.


Increment and Decrement: The Walking Pattern

main.c
Run Online
#include <stdio.h>
int main(void) {
    int nums[5] = {
        10, 20, 30, 40, 50
    };
    int *p = nums;
    printf("Start  : %d  (address %p)\n", *p,(void *)p);
    p++;
    // step to the next int    printf("p++    : %d  (address %p)\n", *p, (void *)p);
    p += 2;
    // step two more (moves 8 bytes)    printf("p += 2 : %d  (address %p)\n", *p, (void *)p);
    p--;
    // back one    printf("p--    : %d  (address %p)\n", *p, (void *)p);
    return 0;
}

Sample output:

main.c
Run Online
Start : 10(address 0x7ffd3c1a4b20)p++ : 20(address 0x7ffd3c1a4b24) ← +4 bytesp += 2 : 40(address 0x7ffd3c1a4b2c) ← +8 bytes from previousp-- : 30(address 0x7ffd3c1a4b28) ← -4 bytes

Look at the address increments: 4 bytes for each int step, never 1. That's the type scaling in action.

Choosing an idiomatic shorthand:p += 2 moves forward two elements — it is not "add 2 bytes." This is where the intuition clicks for most people: p += n means "skip n elements."

(Suggested image: the array with a pointer marker stepping across it, addresses below changing by 4 each time)


The Famous Equivalence: arr[i] ≡ *(arr + i)

This is the payoff moment. The C standard defines array indexing in terms of pointer arithmetic:

Not "similar to" — defined as equivalent. The compiler translates the first into the second.

main.c
Run Online
int nums[4] = {
    10, 20, 30, 40
};
int *p = nums;
// All three of these print the SAME value:printf("%d\n", nums[2]);       // array notationprintf("%d\n", *(nums + 2));   // pointer notationprintf("%d\n", *(p + 2));      // same thing via a pointer
// And these are equal because addition commutes:printf("%d\n", *(2 + nums));   // ✓ legal! — the same as *(nums + 2)printf("%d\n", 2[nums]);       // ✓ also legal (but never write this!)

That last line is real: since arr[i] is *(arr + i) and addition is commutative, 2[nums] is valid C that does exactly what nums[2] does. Nobody writes it, but every C programmer should see it once — it proves the equivalence isn't a teaching device, it's literally the mechanism.

Two practical consequences:

  1. You can use either style freely. Index-based loops and pointer-based loops produce identical machine code with optimization on. Choose by readability.
  2. Everything makes sense at last. Passing arrays to functions (decay = passing &arr[0]), sizeof failing on parameters, fgets(buf, ...) filling a buffer, scanf("%d", &x) storing through a pointer — all the same mechanism.

(Suggested image: the equivalence shown as an equation over the memory diagram — nums[2] on the left transforming into *(nums + 2) on the right with the same cell highlighted. This is the page's key teaching graphic.)


Traversal: Index Loops vs Pointer Loops

Same task, two styles:

main.c
Run Online
int nums[5] = {
    10, 20, 30, 40, 50
};
// --- Index style ---for (int i = 0; i < 5; i++) {    printf("%d ", nums[i]);}
// --- Pointer style ---int *end = nums + 5;                    // one past the LAST elementfor (int *p = nums; p < end; p++) {    printf("%d ", *p);}

Both print 10 20 30 40 50 and compile to essentially the same code. The pointer version has one detail worth understanding: nums + 5 is legal to compute and use as a bound, even though it points one past the array. You just must never dereference it.

The one-past-the-end rule: you may form a pointer to one element beyond an array (and compare against it, and use it as a stopping boundary), but you may not read or write there:

main.c
Run Online
int *end = nums + 5;
// ✓ legal — a valid boundary marker*end = 99;
// ✗ UNDEFINED BEHAVIOR — writing past the array

This rule exists precisely to make the p < end loop above well-defined. It's a small, slightly odd corner of the standard, and knowing it marks you as someone who's actually read the rules.

(Suggested image: the array with a dashed "one past the end" box marked legal-as-a-boundary but not-dereferenceable)


Pointer Subtraction: Measuring Distance

Subtract one pointer from another and you get the number of elements between them:

main.c
Run Online
#include <stdio.h>
int main(void) {
    int nums[6] = {
        10, 20, 30, 40, 50, 60
    };
    int *first = &nums[1];
    int *fifth = &nums[5];
    printf("Distance: %ld elements\n", fifth - first);
    // 4
    // The practical use: converting a search result into an index    int *found = &nums[3];    printf("Found at index: %ld\n", found - nums);           // 3
    return 0;
}

Output:

main.c
Run Online
Distance: 4 elementsFound at index: 3

Note the units again: fifth - first is 4, not 16, even though the addresses differ by 16 bytes. Pointer subtraction divides by the element size — it reports distances in elements, which is what you actually want.

This is the standard way to turn a pointer back into an index. You saw it in the string functions tutorial:

main.c
Run Online
char *pos = strstr(text, "world");
if (pos != NULL) {
    printf("Found at index %ld\n", pos - text);
    // pointer → index
}

Search functions return pointers because that's the natural way to walk memory; subtraction converts back to a position when you need one. (%ld or %td for the difference — technically ptrdiff_t.)

One critical restriction: both pointers must point into the same array (or one past its end). Subtracting pointers into unrelated objects is undefined — the result is meaningless.


Comparisons: What's Legal

main.c
Run Online
int nums[5] = {
    10, 20, 30, 40, 50
}
;
int *p = nums;
int *q = nums + 3;
if (p < q) printf("p comes before q\n");
// ✓ legalif (p == q) printf("same element\n");
// ✓ legalif (p != nums) printf("moved\n");
// ✓ legal

Comparisons (<, >, <=, >=, ==, !=) are meaningful between pointers into the same array — earlier elements compare "less than" later ones. That's how p < end loops work.

Comparing pointers into different objects is undefined by the standard (in practice you sometimes get the numeric comparison, but don't rely on it). Comparing against NULL is always fine — if (p != NULL) is universal.


A Complete Example: Rewriting String Functions With Pointers

The best way to internalize pointer arithmetic is to reimplement library functions using it. Here are strlen and strcpy written in the classic pointer style:

main.c
Run Online
#include <stdio.h>
// --- strlen via pointer arithmetic ---int myStrlen(const char *s) {    const char *start = s;                  // remember the beginning    while (*s != '\0') {                     // walk until the terminator        s++;    }    return (int)(s - start);                 // distance = length}
// --- strcpy via pointer arithmetic ---char *myStrcpy(char *dest, const char *src) {    char *original = dest;                   // remember dest for the return value
while (*src != '\0') {
    *dest = *src;
    // copy one character        dest++;        src++;    }    *dest = '\0';                            // terminate the destination
    return original;
    // library strcpy returns dest}
    // --- The famously terse version (real code uses variants of this) ---char *myStrcpyCompact(char *dest, const char *src) {    char *original = dest;    while ((*dest++ = *src++));              // copy, then step both; stop at '\0'    return original;}
    int main(void) {
        char source[] = "Pointer arithmetic";
        char copy1[50];
        char copy2[50];
        printf("Length     : %d\n", myStrlen(source));
        myStrcpy(copy1, source);
        printf("Copy 1     : %s\n", copy1);
        myStrcpyCompact(copy2, source);
        printf("Copy 2     : %s\n", copy2);
        // Now using pointer arithmetic to reverse a string in place    char word[] = "stressed";    char *left = word;    char *right = word + myStrlen(word) - 1;
        while (left < right) {
            char temp = *left;
            *left++ = *right;
            // assign, then move left forward        *right-- = temp;                      // assign, then move right backward    }
            printf("Reversed   : %s\n", word);
            // "desserts"
            return 0;
        }

Sample Run:

main.c
Run Online
Length : 18Copy 1 : Pointer arithmeticCopy 2 : Pointer arithmeticReversed : desserts

Several techniques packed in here:

  1. myStrlen saves the start pointer, walks to the terminator, then subtracts to get the length. That's pointer subtraction used exactly as intended.
  2. myStrcpy returns the original dest — because you destroyed your pointer by walking it. Saving the start before advancing is the essential habit with pointer loops.
  3. while ((*dest++ = *src++)); — the compact copy idiom. Copy the character, advance both pointers, and stop when the copied value is '\0' (which is falsy). It looks cryptic the first time and becomes perfectly readable once you can parse *dest++ as "dereference dest, then advance dest." Worth noting: some compilers warn about assignments in conditions; the extra parentheses are what suppress that.
  4. The in-place reverse with two pointers meeting in the middle — *left++ = *right assigns and steps in one expression. This pattern (two pointers converging) is a genuinely common algorithm technique.

(Suggested image: the reverse operation step by step — left and right pointers moving toward each other, characters swapping, with the *left++ expansion labeled)


The Precedence Trap: *p++ vs (*p)++ vs ++*p

These three look nearly identical and do three different things. This is a favorite interview question for a reason:

main.c
Run Online
int nums[3] = {
    10, 20, 30
}
;
int *p = nums;
ExpressionWhat happensResult
*p++Dereference p, then advance pgives 10; p now points at 20
(*p)++Dereference p, then increment the value at that addressgives 10; nums[0] becomes 11; p unchanged
++*pIncrement the value, then dereferencenums[0] becomes 11; p unchanged
*++pAdvance p first, then dereferencep points at 20; gives 20

The key: postfix ++ binds tighter than *, so *p++ means *(p++). To increment the pointed-to value instead, you need the parentheses: (*p)++.

main.c
Run Online
int *p = nums;
printf("%d\n", *p++);
// 10 — and p moves to point at 20printf("%d\n", *p);        // 20
p = nums;
// resetprintf("%d\n", (*p)++);    // 10 — but nums[0] is now 11printf("%d\n", nums[0]);   // 11

When in doubt, add parentheses. Explicit beats clever, and this is exactly the kind of expression where an extra pair clarifies for both you and the next reader.

(Suggested image: a small table graphic showing the four expressions with arrows indicating what moves — the pointer or the value)


Arithmetic on Typed Arrays: When Types Matter Most

Pointer arithmetic on different types, side by side:

main.c
Run Online
char c[] = "Hello";
int n[] = {
    1, 2, 3, 4
};
double d[] = {
    1.1, 2.2, 3.3
};
char *cp = c;
int *np = n;
double *dp = d;
printf("%p %p\n",(void *)cp,(void *)(cp + 1));
// differ by 1 byte
printf("%p %p\n",(void *)np,(void *)(np + 1));
// differ by 4 bytes
printf("%p %p\n",(void *)dp,(void *)(dp + 1));
// differ by 8 bytes

You can verify this yourself — the printed addresses will differ by exactly sizeof(element) each time. It's a good exercise for building intuition: the same + 1, three different distances.


Common Pointer Arithmetic Mistakes

  1. Assuming p + 1 adds one byte — it adds one element (4 bytes for int, 8 for double). The address numbers show it clearly.
  2. Losing the base pointer — after p++ repeatedly, you can no longer free or return to the start. Save char *start = p; or int *base = arr; before walking.
  3. Dereferencing one-past-the-end — *(arr + n) is undefined behavior. Compute it as a bound, never dereference it.
  4. Adding two pointers — meaningless, and a compile error. Only add integers to pointers, subtract pointers from pointers.
  5. Subtracting pointers from different arrays — undefined. The result is only meaningful within one array.
  6. The *p++ precedence trap — *p++ advances the pointer; (*p)++ modifies the value. Parenthesize when it matters.
  7. Walking off the end in pointer loops — the p < end form with end = arr + n is safest. Avoid while (*p) style loops on non-string data — they only stop at a zero value, which may be data rather than a terminator.
  8. Ignoring compiler warnings — pointer type mismatches produce warnings for a reason; they signal real size errors. Compile with -Wall -Wextra.
  9. Mixing up pointer and value in function calls — swap(*a, *b) vs swap(a, b). (From the pointers pillar page.)

(Suggested image: "9 Pointer Arithmetic Mistakes" checklist — with the p+1 scaling and the one-past-the-end rule as the two emphasized items)


Final Thoughts

Pointer arithmetic is where C's memory model becomes tangible. One rule covers nearly everything: arithmetic scales by the pointed-to type, so p + 1 means "the next element," not "the next byte." From that single fact, the arr[i] ≡ *(arr + i) equivalence follows, and the equivalence explains arrays, function parameters, sizeof behavior, and half the standard library.

The habits worth keeping: save the base pointer before walking, use arr + n as a one-past-the-end bound and never dereference it, subtract same-array pointers to get distances in elements, and parenthesize when mixing * with ++. Those four habits prevent essentially every pointer-arithmetic bug I've listed.

Honest note to close on: modern, readable C usually prefers indexing (arr[i]) over pointer walking, because it's clearer and optimizes identically. The value in learning pointer arithmetic isn't so you'll write *dest++ = *src++ everywhere — it's that you'll be able to read it when other people do, and understand what the machine is actually doing underneath.

Next in this cluster: pointers and arrays in depth — including the two-dimensional case, where pointer arithmetic gets genuinely interesting.


Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
int main() {
    int arr[] = {
        10, 20, 30, 40, 50
    };
    int *ptr = arr;
    // Points to arr[0]
    printf("Initial value (*ptr): %d (Address: %p)\
", *ptr,(void*)ptr);
    ptr++;
    // Advances by 4 bytes
    printf("After ptr++ (*ptr):   %d (Address: %p)\
", *ptr,(void*)ptr);
    ptr += 2;
    // Advances by 8 bytes
    printf("After ptr+=2 (*ptr):  %d (Address: %p)\
", *ptr,(void*)ptr);
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Perform pointer arithmetic only within the bounds of contiguous arrays to prevent undefined pointer wrap-around.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: If `int *p = (int*)0x1000;`, what is the address value of `p + 2` assuming 4-byte integers?

A: `0x1008` (0x1000 + 2 * 4 = 0x1008).
Q

Q: Can you perform pointer arithmetic on a `void*` pointer in standard ISO C?

A: No, because `void` has no size (`sizeof(void)` is incomplete). GCC allows it as an extension treating `void*` as 1 byte.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations