Pointer Arithmetic & Arrays
Why This Matters
Arrays and pointers are the Batman and Robin of C. They're not the same thing, but they work so closely together that understanding their relationship unlocks a deeper level of programming power. When you pass an array to a function, when you iterate with pointer notation, when you implement strlen or memcpy—all of these depend on the array-pointer connection. This lesson bridges the gap between the theory you learned in the Pointers and Arrays lessons and the practical skills you need every day.
In the Linux kernel source code, you'll rarely see arr[i]. Kernel developers prefer *(arr + i) and pointer arithmetic because it's more explicit about what's happening at the hardware level. Understanding both notations means you can read and write idiomatic systems code.
1. Pointer Arithmetic: The Compiler Does the Math For You
When you add an integer to a pointer, something magical—but completely logical—happens. The compiler scales the integer by the size of whatever the pointer points to:
int *ip; // ip points to integers (4 bytes each)
char *cp; // cp points to characters (1 byte each)
double *dp; // dp points to doubles (8 bytes each)
ip + 1; // advances the address by 4 bytes (sizeof(int))
cp + 1; // advances the address by 1 byte (sizeof(char))
dp + 1; // advances the address by 8 bytes (sizeof(double))
This scaling is why ptr++ "just works" to move to the next array element regardless of type. The compiler knows the type, so it knows how many bytes to skip.
Here's a concrete demonstration:
#include <stdio.h>
int main() {
int arr[3] = {10, 20, 30};
int *ptr = arr; // arr decays to &arr[0]
printf("First element: address=%p, value=%d
", (void*)ptr, *ptr);
ptr++; // moves forward by sizeof(int) = 4 bytes
printf("Second element: address=%p, value=%d
", (void*)ptr, *ptr);
ptr++; // moves forward another 4 bytes
printf("Third element: address=%p, value=%d
", (void*)ptr, *ptr);
return 0;
}
Sample output:
First element: address=0x7fff1000, value=10
Second element: address=0x7fff1004, value=20
Third element: address=0x7fff100c, value=30
Each ptr++ moves the pointer forward by exactly 4 bytes—the size of one int. This is scaling in action.
2. The Deep Secret: arr[i] IS *(arr + i)
Here is the single most important fact about arrays and pointers in C: the bracket notation arr[i] is syntactic sugar for *(arr + i). The compiler translates one into the other. These two lines produce identical machine code:
int x = arr[3]; // readable form
int x = *(arr + 3); // what the compiler actually does
This means you can use pointer arithmetic anywhere you'd use brackets, and vice versa. All of the following are equivalent:
arr[0] *(arr + 0) *arr // all give the first element
arr[i] *(arr + i) i[arr] // YES, i[arr] is valid C!
Wait, i[arr]? That's not a typo. Since arr[i] is *(arr + i), and addition is commutative (arr + i = i + arr), then i[arr] must equal *(i + arr) = *(arr + i) = arr[i]. Please don't write i[arr] in production code, but knowing it works deepens your understanding of the underlying mechanism.
3. Array-Pointer Equivalence Table
Here's a quick reference for all the common operations:
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // or: int *p = &arr[0];
// Accessing elements — all of these pairs are equivalent:
arr[0] <==> *p
arr[i] <==> *(p + i) <==> p[i]
arr[i] <==> *(arr + i)
// Getting addresses — all equivalent:
&arr[0] <==> p <==> arr
&arr[i] <==> p + i <==> arr + i
4. Array Decay: What Happens When You Pass an Array to a Function
This is the single most confusing behavior in C, and you must memorize it: when you pass an array as a function argument, it "decays" into a pointer to its first element.
The function does not receive a copy of the array. It receives an 8-byte address. The size information is lost. This has two critical consequences:
- You must pass the array size as a separate argument—the function cannot compute it.
- Any changes the function makes to array elements modify the original array—because the function is working directly on the original memory.
#include <stdio.h>
// These three function signatures are IDENTICAL to the compiler:
// void printArray(int arr[], int size)
// void printArray(int arr[10], int size) // the 10 is IGNORED!
// void printArray(int *arr, int size)
void doubleArray(int *arr, int size) {
printf("Inside function, sizeof(arr) = %zu (NOT the array size!)
",
sizeof(arr)); // prints 8 (pointer size), NOT 20!
for (int i = 0; i < size; i++) {
arr[i] *= 2; // modifies the ORIGINAL array
}
}
int main() {
int data[5] = {1, 2, 3, 4, 5};
printf("Before: ");
for (int i = 0; i < 5; i++) printf("%d ", data[i]);
printf("
");
doubleArray(data, 5); // data decays to &data[0]
printf("After: ");
for (int i = 0; i < 5; i++) printf("%d ", data[i]);
printf("
"); // prints 2 4 6 8 10
return 0;
}
Three important things to notice:
- The function signatures
int arr[]andint *arrare completely interchangeable in function parameters. sizeof(arr)inside the function returns 8 (pointer size), not 20 (array size). The array size information is gone.- The original
dataarray is permanently modified because the function works on the actual memory, not a copy.
5. Pointer Subtraction: Finding the Distance Between Elements
Just as you can add integers to pointers, you can subtract one pointer from another if they point into the same array. The result is the number of elements between them (not bytes):
int arr[5] = {10, 20, 30, 40, 50};
int *p1 = &arr[1]; // points to 20
int *p2 = &arr[4]; // points to 50
printf("%td
", p2 - p1); // prints 3 (three elements between them)
// p2 - p1 = (address of arr[4] - address of arr[1]) / sizeof(int)
// = (16 bytes) / 4 = 3 elements
This is how strlen could be implemented: find the null terminator, subtract the start pointer, and you have the string length.
6. Memory Diagram: Walking Through an Array with a Pointer
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // p points to arr[0]
Step 0: p = arr (points to address 0x1000)
+-------+-------+-------+-------+-------+
| 10 | 20 | 30 | 40 | 50 |
+-------+-------+-------+-------+-------+
0x1000 0x1004 0x1008 0x100C 0x1010
^
p
Step 1: p++ (p now points to address 0x1004)
+-------+-------+-------+-------+-------+
| 10 | 20 | 30 | 40 | 50 |
+-------+-------+-------+-------+-------+
0x1000 0x1004 0x1008 0x100C 0x1010
^
p
Step 2: p += 2 (p now points to address 0x100C)
+-------+-------+-------+-------+-------+
| 10 | 20 | 30 | 40 | 50 |
+-------+-------+-------+-------+-------+
0x1000 0x1004 0x1008 0x100C 0x1010
^
p
7. The Difference Between Arrays and Pointers
Despite their close relationship, arrays and pointers are not the same thing. Here's the crucial distinction:
int arr[5]; // arr is an ARRAY. Memory for 5 ints is allocated.
int *ptr; // ptr is a POINTER. Memory for 1 address (8 bytes) is allocated.
sizeof(arr); // 20 (5 * 4 bytes) — the full array size
sizeof(ptr); // 8 (address size on 64-bit) — just the pointer size
&arr; // pointer to array of 5 ints (type: int (*)[5])
&ptr; // pointer to pointer to int (type: int **)
arr = something; // COMPILE ERROR! Array names are not assignable
ptr = something; // OK. Pointers can be reassigned
An array name is a constant that refers to a fixed block of memory. You cannot change where an array "points." A pointer is a variable that holds an address and can be reassigned at any time.
8. Common Mistakes
Mistake #1: Using sizeof on a decayed array parameter
void process(int arr[]) {
for (int i = 0; i < sizeof(arr)/sizeof(arr[0]); i++) // BUG!
arr[i] = 0;
}
// sizeof(arr) inside the function = 8 (pointer size), so the loop
// only processes 2 elements (8/4) regardless of the actual array size.
Mistake #2: Returning a pointer to a local array
int* makeArray() {
int local[10] = {1,2,3,4,5,6,7,8,9,10};
return local; // DANGER: local array dies when function returns!
}
Mistake #3: Confusing pointer increment with value increment
int arr[] = {10, 20, 30};
int *p = arr;
*p++; // increments p, then dereferences OLD p (returns 10, p now at 20)
(*p)++; // dereferences p, increments the VALUE (changes 20 to 21)
The precedence matters! *p++ is *(p++) because postfix ++ binds tighter than *.
9. Key Takeaways
- Pointer arithmetic scales automatically:
ptr + nmoves the pointer byn × sizeof(*ptr)bytes. arr[i]is syntactic sugar for*(arr + i). The bracket notation is a convenience; the compiler treats them identically.- Arrays decay to pointers when passed to functions. The size information is lost, and changes affect the original array.
- Arrays and pointers are different: An array is a fixed block of memory; a pointer is a variable holding an address.
sizeofreveals the difference. - Pointer subtraction gives the number of elements between two pointers into the same array, automatically dividing by the element size.
- Precedence traps:
*p++increments the pointer;(*p)++increments the value. When in doubt, use parentheses.
10. Practice Exercises
Exercise 1: Pointer Traversal
Write a program that declares int arr[] = {5, 10, 15, 20, 25}; and traverses it using ONLY pointer arithmetic (no arr[i]). Print each value and its address. Use a pointer that starts at arr and increments until it passes the end (&arr[5]).
Exercise 2: Implement strlen
Write your own function int my_strlen(const char *s) that returns the length of a string using pointer arithmetic—no array brackets allowed. Test it with several strings including an empty string "".
Exercise 3: Array Sum via Pointer
Write a function int sumArray(int *arr, int size) that sums all elements of an integer array using pointer arithmetic (no brackets). The function should work correctly when called from main.
Exercise 4: Reverse Array with Pointers
Write a function void reverse(int *arr, int size) that reverses an array in place using two pointers: one at the start, one at the end, walking toward each other and swapping values. No array brackets allowed.