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).
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
printfoutputs. Ifppoints 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 + nmovesn * sizeof(*p)bytes forward, notnbytes. This means pointer arithmetic automatically scales by the element size. Forint *p,p + 1advances 4 bytes. Fordouble *q,q + 1advances 8 bytes. C does this so you never have to think about byte sizes when walking through arrays.
double *q points to address 0x2000, what address does q + 2 point to? (Assume sizeof(double) is 8.)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 likewhile (*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
sizeofthe type. Ifpandqare bothint *pointers 16 bytes apart,q - preturns 4 (elements), not 16 (bytes). This only works when both pointers point into the same array ormalloc‘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.
void process(int arr[]), what does sizeof(arr) return on a 64-bit system?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
.lengthfield 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.