Pointer Arithmetic & Arrays
Arrays and pointers in C are deeply intertwined. An array name decays to a pointer in most expressions. Array indexing is syntactic sugar for pointer arithmetic. Understanding this relationship — and understanding where it breaks — is essential for writing correct C and reading systems code.
Arrays Decay to Pointers
When you use an array name in an expression, it automatically converts (decays) to a pointer to its first element. This happens in almost every context.
#include <stdio.h>
void print_first(int *p) {
printf("first element: %d\n", *p);
}
int main(void) {
int arr[5] = {10, 20, 30, 40, 50};
// arr decays to &arr[0]
int *p = arr; // no & needed — arr IS a pointer here
print_first(arr); // arr decays to pointer when passed
printf("arr: %p\n", (void *)arr);
printf("&arr[0]: %p\n", (void *)&arr[0]);
printf("Same? %s\n", arr == &arr[0] ? "yes" : "no");
return 0;
}
first element: 10
arr: 0x7ffd3c4a1b40
&arr[0]: 0x7ffd3c4a1b40
Same? yes
When you pass an array to a function, the function receives a pointer. This is why C functions cannot know the size of an array argument — the size information is lost in the decay.
arr[i] Is *(arr + i)
Array indexing is defined as pointer arithmetic plus dereference. The expression arr[i] is exactly equivalent to *(arr + i).
#include <stdio.h>
int main(void) {
int arr[5] = {10, 20, 30, 40, 50};
// These are all equivalent
printf("arr[2]: %d\n", arr[2]);
printf("*(arr + 2): %d\n", *(arr + 2));
printf("*(2 + arr): %d\n", *(2 + arr));
printf("2[arr]: %d\n", 2[arr]); // legal but terrible
return 0;
}
arr[2]: 30
*(arr + 2): 30
*(2 + arr): 30
2[arr]: 30
Because addition is commutative, arr[2] and 2[arr] are both valid C. Nobody writes 2[arr] in real code, but understanding why it works proves you understand the pointer arithmetic model.
Pointer Arithmetic Respects Types
When you add an integer to a pointer, the compiler scales the addition by the size of the pointed-to type.
#include <stdio.h>
int main(void) {
int iarr[3] = {100, 200, 300};
double darr[3] = {1.1, 2.2, 3.3};
char carr[3] = {'A', 'B', 'C'};
int *ip = iarr;
double *dp = darr;
char *cp = carr;
printf("int pointer:\n");
printf(" ip = %p\n", (void *)ip);
printf(" ip + 1 = %p (moved %td bytes)\n",
(void *)(ip + 1), (char *)(ip + 1) - (char *)ip);
printf("double pointer:\n");
printf(" dp = %p\n", (void *)dp);
printf(" dp + 1 = %p (moved %td bytes)\n",
(void *)(dp + 1), (char *)(dp + 1) - (char *)dp);
printf("char pointer:\n");
printf(" cp = %p\n", (void *)cp);
printf(" cp + 1 = %p (moved %td bytes)\n",
(void *)(cp + 1), (char *)(cp + 1) - (char *)cp);
return 0;
}
int pointer:
ip = 0x7ffd3c4a1b40
ip + 1 = 0x7ffd3c4a1b44 (moved 4 bytes)
double pointer:
dp = 0x7ffd3c4a1b50
dp + 1 = 0x7ffd3c4a1b58 (moved 8 bytes)
char pointer:
cp = 0x7ffd3c4a1b60
cp + 1 = 0x7ffd3c4a1b61 (moved 1 byte)
This is the key insight: pointer arithmetic is element-based, not byte-based. The compiler handles the byte-level math for you.
Iterating with Pointers
Pointer-based iteration is idiomatic C and is often clearer than index-based iteration for sequential access:
#include <stdio.h>
void print_array(const int *arr, size_t len) {
const int *end = arr + len;
for (const int *p = arr; p < end; p++) {
printf("%d ", *p);
}
printf("\n");
}
// Summing with pointer iteration
long sum_array(const int *arr, size_t len) {
long total = 0;
const int *end = arr + len;
for (const int *p = arr; p < end; p++) {
total += *p;
}
return total;
}
int main(void) {
int data[] = {5, 10, 15, 20, 25};
size_t len = sizeof(data) / sizeof(data[0]);
print_array(data, len);
printf("sum: %ld\n", sum_array(data, len));
return 0;
}
5 10 15 20 25
sum: 75
The pattern const int *end = arr + len creates a one-past-the-end pointer. Comparing p < end is valid — you can point one past the last element of an array. Dereferencing that pointer is undefined behavior, but holding and comparing it is fine.
Multidimensional Arrays
A 2D array in C is an array of arrays. The elements are stored in row-major order — all elements of row 0, then all elements of row 1, and so on.
#include <stdio.h>
int main(void) {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// Access with indices
printf("matrix[1][2] = %d\n", matrix[1][2]); // 7
// Memory layout is contiguous
int *flat = &matrix[0][0];
for (int i = 0; i < 12; i++) {
printf("%2d ", flat[i]);
}
printf("\n");
// Row addresses
for (int r = 0; r < 3; r++) {
printf("row %d starts at %p\n", r, (void *)matrix[r]);
}
return 0;
}
matrix[1][2] = 7
1 2 3 4 5 6 7 8 9 10 11 12
row 0 starts at 0x7ffd3c4a1b00
row 1 starts at 0x7ffd3c4a1b10
row 2 starts at 0x7ffd3c4a1b20
Each row starts 16 bytes (4 ints * 4 bytes) after the previous one. matrix[1][2] is the element at row 1, column 2, which is byte offset (1 * 4 + 2) * sizeof(int) from the start.
Passing 2D Arrays to Functions
You must specify all dimensions except the first. The compiler needs the column count to compute row offsets:
// Must specify the column count
void print_matrix(int rows, int cols, int mat[][4]) {
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
printf("%3d ", mat[r][c]);
}
printf("\n");
}
}
C99 also supports VLA syntax: int mat[rows][cols], where both dimensions are parameters. Without the column count, mat[1] does not know how many bytes to skip.
Pointers to Pointers: int**
A pointer to a pointer holds the address of a pointer variable. The most common use is arrays of strings (arrays of char *):
#include <stdio.h>
void print_strings(char **strings, int count) {
for (int i = 0; i < count; i++) {
printf("[%d]: %s\n", i, strings[i]);
}
}
int main(void) {
// Array of string pointers
char *languages[] = {"C", "Python", "Rust", "Go"};
int count = sizeof(languages) / sizeof(languages[0]);
print_strings(languages, count);
// This is what main receives:
// int main(int argc, char **argv)
// argv is a pointer to an array of string pointers
return 0;
}
[0]: C
[1]: Python
[2]: Rust
[3]: Go
char ** is "pointer to pointer to char." languages[i] dereferences the first pointer (getting a char *), and the string functions dereference the second pointer (getting individual char values).
Where Array/Pointer Equivalence Breaks
Arrays and pointers are NOT the same thing. They behave identically in expressions (because of decay), but they differ in two important ways.
sizeof
#include <stdio.h>
int main(void) {
int arr[10];
int *p = arr;
printf("sizeof(arr): %zu\n", sizeof(arr)); // 40 (10 * 4 bytes)
printf("sizeof(p): %zu\n", sizeof(p)); // 8 (pointer size)
// This is how you compute array length
size_t len = sizeof(arr) / sizeof(arr[0]);
printf("length: %zu\n", len); // 10
return 0;
}
sizeof(arr): 40
sizeof(p): 8
length: 10
sizeof(arr) gives the total byte size of the array. sizeof(p) gives the size of the pointer itself (8 bytes on 64-bit). After decay, the size information is gone.
& (Address-of)
#include <stdio.h>
int main(void) {
int arr[5];
// arr and &arr have the same address but different types
printf("arr: %p\n", (void *)arr); // pointer to int
printf("&arr: %p\n", (void *)&arr); // pointer to int[5]
printf("&arr+1: %p\n", (void *)(&arr + 1));// moves by 20 bytes (5 * 4)
printf("arr+1: %p\n", (void *)(arr + 1)); // moves by 4 bytes
return 0;
}
arr: 0x7ffd3c4a1b40
&arr: 0x7ffd3c4a1b40
&arr+1: 0x7ffd3c4a1b54
arr+1: 0x7ffd3c4a1b44
arr decays to int * — adding 1 moves 4 bytes. &arr is int (*)[5] (pointer to array of 5 ints) — adding 1 moves 20 bytes (the entire array). Same address, different type, different arithmetic.
Pointer Subtraction
Subtracting two pointers of the same type gives the number of elements between them. &arr[7] - &arr[2] is 5 (elements, not bytes). Use ptrdiff_t (from <stddef.h>) for the result type. Subtracting pointers that do not point into the same array is undefined behavior.
Common Pitfalls
- Using sizeof on a decayed array — inside a function that receives
int *arr,sizeof(arr)returns the pointer size (8), not the array size. Always pass the length separately. - Pointer arithmetic on void* — you cannot do arithmetic on
void *because the compiler does not know the element size. Cast tochar *first for byte-level arithmetic. - Out-of-bounds pointer arithmetic — you may point one past the end of an array, but no further. Going two past the end or before the beginning is undefined behavior, even without dereferencing.
- Confusing int with int[][N]** —
int **is a pointer to a pointer;int [][N]is a 2D array. They have different memory layouts and are not interchangeable. - Forgetting that string literals are arrays —
"hello"isconst char[6](including the null terminator). It decays toconst char *in expressions.
Key Takeaways
- Arrays decay to pointers to their first element in most expressions. This is why you can use array names where pointers are expected.
arr[i]is exactly*(arr + i). Array indexing is pointer arithmetic with syntactic sugar.- Pointer arithmetic scales by the pointed-to type size.
int *p + 1movessizeof(int)bytes. - Multidimensional arrays are arrays of arrays, stored contiguously in row-major order.
int **(pointer to pointer) is used for arrays of strings and dynamically allocated 2D data.- Array/pointer equivalence breaks for
sizeof(arrays know their size, pointers do not) and&(different types, different arithmetic). - Always pass array length as a separate parameter. C arrays do not carry their size.