pointers-memory Lesson 3 22 min read

How Does Pointer Arithmetic Work and Connect to Arrays?

ptr + 1 doesn't add 1 byte — it adds sizeof the type. And arr[i] is just *(arr + i).

Reading: C Text: Ch. 7 §5 (pp. 450–460, Arrays as Function Arguments)

After this lesson, you will be able to:

  • Calculate the result of adding an integer to a pointer, accounting for type-size scaling
  • Explain the equivalence between arr[i] and *(arr + i)
  • Traverse an array using pointer increment (p++) instead of index-based access
  • Distinguish between *p++, (*p)++, *++p, and ++*p
  • Explain why an array parameter decays to a pointer inside a function

Arrays and Pointers Are Secretly the Same Thing

Here’s one of the most surprising facts about C: array indexing is just pointer arithmetic in disguise. When you write arr[3], the compiler translates it to *(arr + 3). They’re identical — same machine code, same result.

This means once you understand pointer arithmetic, you understand arrays at a deeper level. And when you pass an array to a function, you’re really passing a pointer.


How Pointer Math Works

Adding to a Pointer

When you add 1 to a pointer, it doesn’t move by 1 byte — it moves by sizeof the pointed-to type:

Before you read further: look at the code below and predict what each printf outputs. If p points to the start of the array, what does *(p + 1) give you? Then read the explanation.

int arr[] = {10, 20, 30, 40, 50};
int *p = arr;          // Points to arr[0]

printf("%d\n", *p);         // 10  (arr[0])
printf("%d\n", *(p + 1));   // 20  (arr[1])
printf("%d\n", *(p + 2));   // 30  (arr[2])

Since int is 4 bytes, p + 1 moves 4 bytes forward — exactly to the next int:

Memory:  [10] [20] [30] [40] [50]
Address: 1000 1004 1008 1012 1016
         ↑    ↑    ↑
         p    p+1  p+2

Key Insight: p + n moves n * sizeof(*p) bytes forward, not n bytes. This means pointer arithmetic automatically scales by the element size. For int *p, p + 1 advances 4 bytes. For double *q, q + 1 advances 8 bytes. C does this so you never have to think about byte sizes when walking through arrays.

Check Your Understanding
If double *q points to address 0x2000, what address does q + 2 point to? (Assume sizeof(double) is 8.)
A 0x2002
B 0x2008
C 0x2010
D 0x2004
Answer: C. Pointer arithmetic scales by sizeof(double) = 8 bytes. So q + 2 advances 2 * 8 = 16 bytes. 0x2000 + 16 = 0x2000 + 0x10 = 0x2010. Option A adds 2 bytes (ignoring type size). Option B adds 8 (only one element). Option D uses sizeof(int) instead of sizeof(double).

The Array-Pointer Equivalence

These are interchangeable:

Array notation Pointer notation Meaning
arr[0] *arr First element
arr[i] *(arr + i) Element at index i
&arr[i] arr + i Address of element i

The name of an array, when used in an expression, decays to a pointer to its first element. That’s why you can write:

int arr[] = {10, 20, 30};
int *p = arr;             // No & needed — arr decays to pointer

Walking an Array with Pointers

Instead of indexing, you can increment a pointer:

int arr[] = {10, 20, 30, 40, 50};
int size = 5;

// Index-based (familiar):
for (int i = 0; i < size; i++)
{
    printf("%d ", arr[i]);
}

// Pointer-based (equivalent):
int *p = arr;
for (int i = 0; i < size; i++)
{
    printf("%d ", *p);
    p++;                   // Move to next element
}

Both print: 10 20 30 40 50

The *p++ Trap: Operator Precedence with Pointers

The placement of * and ++ changes meaning dramatically. This trips up even experienced programmers:

Expression What happens Effect
*p++ Dereference p, then increment the pointer Reads current value, then moves p to next element. Equivalent to *(p++).
(*p)++ Increment the value that p points to The pointer doesn’t move — the data changes.
++*p Same as (*p)++ but pre-increment Increments the value first, then returns the new value.
*++p Increment pointer first, then dereference Skips one element, then reads. Equivalent to *(++p).
int arr[] = {10, 20, 30};
int *p = arr;

printf("%d\n", *p++);     // Prints 10, then p moves to arr[1]
printf("%d\n", *p);        // Prints 20 (p is now at arr[1])

(*p)++;                     // arr[1] becomes 21 — pointer stays put
printf("%d\n", *p);        // Prints 21

Common Pitfall: *p++ is idiomatic C — you’ll see it in string-processing loops like while (*src) *dest++ = *src++;. But if you mean to increment the value (not the pointer), you need (*p)++ with explicit parentheses. When in doubt, use parentheses to make your intent clear.

Deep dive: Why does *p++ dereference first?

In C’s operator precedence, postfix ++ binds tighter than unary *. So *p++ parses as *(p++) — the pointer increments, but since it’s post-increment, the old address is used for the dereference. The new pointer value only takes effect after the full expression is evaluated. This is the same reason i++ in arr[i++] uses the old index for the access.

Pointer Subtraction

Subtracting two pointers gives the number of elements between them (not bytes):

int arr[] = {10, 20, 30, 40, 50};
int *start = &arr[0];
int *end = &arr[4];

printf("Elements between: %ld\n", end - start);    // 4

Key Insight: Pointer subtraction is the inverse of pointer addition — both scale by sizeof the type. If p and q are both int * pointers 16 bytes apart, q - p returns 4 (elements), not 16 (bytes). This only works when both pointers point into the same array or malloc‘d block. Subtracting pointers to unrelated memory is undefined behavior.

Why Arrays Decay in Functions

This is why functions can’t use sizeof on array parameters:

void print_size(int arr[])
{
    // sizeof(arr) is the size of a POINTER (8 bytes), not the array!
    printf("Inside function: %zu\n", sizeof(arr));    // 8
}

int main(void)
{
    int nums[100];
    printf("In main: %zu\n", sizeof(nums));           // 400 (100 * 4)
    print_size(nums);
    return 0;
}

Key Insight: When you pass an array to a function, it decays to a pointer. The function receives int *arr, not the full array. That’s why you must always pass the size as a separate parameter — the function has no way to determine it from the pointer alone.

Check Your Understanding
Inside a function declared as void process(int arr[]), what does sizeof(arr) return on a 64-bit system?
A The total size of the original array in bytes
B The number of elements in the array
C 4 (the size of one int)
D 8 (the size of a pointer)
Answer: D. Despite the int arr[] syntax, the parameter is actually int *arr — the array decayed to a pointer. On a 64-bit system, pointers are 8 bytes. The original array's size information is lost at the function boundary. This is why C functions always need a separate size parameter.
Why does this matter?

Array decay is the root cause of buffer overflows — one of the most exploited security vulnerabilities in C. If a function doesn’t know how big its array is, it can’t check bounds. Always pass the size alongside the pointer. This pattern will follow you through every lab and every C project.

From Java: In Java, arrays are objects with a .length field that travels with the array. In C, the array name decays to a raw pointer when passed to a function — all size information is lost. This is the root of why C requires you to pass size everywhere.

Pointer Comparison

You can compare pointers to check their relative positions:

int arr[5];
int *p = &arr[1];
int *q = &arr[3];

if (p < q)     // true — p points to an earlier element
{
    printf("p comes before q\n");
}

This only makes sense for pointers into the same array.

Deep dive: const with pointers — three different meanings

The keyword const can appear in different positions with pointers, and each position means something different:

Declaration What’s constant? Can you change the value? Can you reassign the pointer?
const int *p The data No (*p = 5 is an error) Yes (p = &other is fine)
int * const p The pointer Yes (*p = 5 is fine) No (p = &other is an error)
const int * const p Both No No

Reading trick: read the declaration right-to-left. const int *p → “p is a pointer to an int that is const” (can’t change the int). int * const p → “p is a const pointer to an int” (can’t change where p points).

You’ll see const int * heavily in function parameters — it promises the function won’t modify the caller’s data:

void print_array(const int *arr, int size)   // "I won't modify your array"
{
    arr[0] = 99;   // COMPILE ERROR — arr points to const data
}

This is the C equivalent of Java’s final on parameters, but more precise — Java’s final prevents reassigning the reference, while C’s const int * prevents modifying the data.

Quick Check: If int *p points to address 0x1000, what address does p + 3 point to?

0x100C (which is 0x1000 + 3 * 4 = 0x1000 + 12). Each int is 4 bytes, so p + 3 advances by 12 bytes.

Quick Check: Why is arr[i] exactly the same as *(arr + i)?

The C language defines array indexing as pointer arithmetic. arr[i] is literally syntactic sugar for *(arr + i). The compiler generates the same code for both. This works because arr decays to a pointer to its first element, and + i moves by i elements.

Quick Check: Why can't a function determine an array's size from its parameter?

When passed to a function, the array decays to a pointer. sizeof(arr) inside the function returns the size of the pointer (typically 8 bytes), not the array. The size information is lost at the function boundary.


Two Notations, One Reality

Array notation (arr[i]) and pointer notation (*(arr + i)) are the same thing. Understanding this connection means you’ll never be confused by C code that mixes them — and you’ll understand why arrays behave the way they do in functions.

Next: double pointers. What happens when a function needs to modify a pointer itself, not just the value it points to? You need a pointer to a pointer — int **pp.

Big Picture: Pointer arithmetic is how C implements arrays, strings, and data structures at the hardware level. When you write arr[i], the compiler generates pointer math. Understanding this connection means you can read any C code that manipulates buffers, parse binary data, and reason about memory layout — skills you’ll use in systems programming, networking, and security.