Pointers & Memory · beginner · ~10 min
- Explain what **array-to-pointer decay** is and name the exact contexts where it happens. - Prove to yourself why `a[i]` and `*(a + i)` produce the identical value and address. - Identify the two places decay does **not** happen — `sizeof` and `&a` — and predict what each yields. - Pass arrays into functions correctly and understand why the length must travel as a separate argument. - Recognise and fix the classic `sizeof(arr)/sizeof(arr[0])`-inside-a-function bug. - Read and write array elements safely by walking a pointer instead of indexing.
An array is a fixed-length block of elements laid out back-to-back in memory. A pointer is a variable that holds an address. These two ideas feel separate, but in C they are wired together so tightly that beginners often confuse them. This lesson makes the connection precise: when you use an array's name in an expression, C almost always converts it into a pointer to its first element. That conversion is called array-to-pointer decay.
This builds directly on Pointer arithmetic. There you learned that adding i to a pointer moves it forward by i elements (not i bytes), because the compiler scales by the element size. That single rule is the engine behind the whole a[i] == *(a + i) equivalence you will see here. If pointer arithmetic still feels shaky, revisit it — everything below leans on it.
In plain language: writing a[i] is just a friendlier way of saying "start at the address of the array, step forward i elements, and look at what is there." Once you internalise that, indexing stops being magic and becomes ordinary arithmetic. The terminology to carry forward is: decay (array name becoming a pointer), element size (what pointer math scales by), and bounds (the valid range 0 .. n-1).
Almost every non-trivial C program stores data in arrays and hands them to functions. The moment you pass an array to a function, it decays to a pointer — and the length is gone. Misunderstanding this is the single most common source of two serious bugs: computing the wrong element count with sizeof inside a function, and reading or writing past the end of the array. The second one is not just a wrong answer; it is undefined behaviour and a classic security hole (buffer overflow).
Understanding decay also unlocks idiomatic C. String functions, qsort, memcpy, and virtually every standard-library routine that touches a buffer takes a pointer plus a length precisely because arrays decay. Once you see arrays and pointers as two views of the same memory, this API style stops looking arbitrary and starts looking obvious.
Definition. In most expressions, an array's name is automatically converted to a pointer to its element 0. The type int[5] becomes int*, and the value is &a[0].
Plain-language explanation. The array name, when you use it (assign it, pass it, do arithmetic on it), evaluates to an address — the address of the first element. It does not copy the whole array; it just yields where the array starts.
How it works internally. An array like int a[5] is 5 contiguous int slots. The compiler knows the base address and the element size. When a decays, you get that base address typed as int*. From there, ordinary pointer arithmetic navigates the rest.
int a[5] = {10, 20, 30, 40, 50};
address: 1000 1004 1008 1012 1016
+------+------+------+------+------+
value: | 10 | 20 | 30 | 40 | 50 |
+------+------+------+------+------+
index: 0 1 2 3 4
a decays to -> 1000 (an int*, pointing at element 0)
a + 2 == 1008 (base + 2 * sizeof(int))
*(a + 2) == 30 == a[2]
When to rely on it / when NOT to. Rely on it every time you pass an array to a function or walk it with a pointer. Do NOT rely on it to remember the length: after decay, the count is not carried anywhere.
Common pitfall. Assuming a decayed pointer "knows" how many elements it points to. It does not. The pointer is just an address; the length lives only in your head (and in a separate variable you must pass along).
Knowledge check: In the expression
int total = a[0] + a[1];, does the arrayadecay to a pointer? (Answer: yes —a[0]is*(a+0), andais used as a value, so it decays.)
a[i] equals *(a + i)Definition. The subscript operator is defined by the C standard as a[i] == *(a + i). It is not "similar" — it is the same operation.
Plain-language explanation. a decays to an address. a + i moves forward i elements. The * reads (or writes) whatever sits there. The [] is pure syntactic sugar over that.
How it works internally. Because addition is commutative, a + i equals i + a, so *(a + i) equals *(i + a), which means i[a] is legal C and equals a[i]. (Never write i[a] in real code — but it proves the point.)
| Expression | Meaning | Yields |
|---|---|---|
a[i] |
subscript | the element's value / lvalue |
*(a + i) |
deref of pointer math | identical to a[i] |
a + i |
pointer arithmetic | address of element i |
&a[i] |
address-of element | also a + i |
Knowledge check (predict the output): Given
int a[3] = {7, 8, 9};, what does*(a + 2)print with%d? (Answer:9.)
Decay is the rule, but two operators see the whole array, not a pointer.
sizeof on a declared array gives the total bytes of the array. On a decayed pointer, it gives the pointer's size (typically 8 on 64-bit systems).
&a (address-of the array) has type "pointer to array of N," e.g. int(*)[5]. It points to the same byte as a/&a[0], but the type differs, so &a + 1 jumps forward by the whole array, not one element.
int a[5];
sizeof(a) -> 20 (5 * sizeof(int)), array NOT decayed
sizeof(&a[0]) -> 8 a pointer's size
a : type int* , a + 1 moves 4 bytes
&a : type int(*)[5] , &a + 1 moves 20 bytes
&a[0] : type int* , same address as a
When to use &a. When you genuinely need a pointer to the whole array (rare for beginners, e.g. passing to a function declared void f(int (*p)[5])). Otherwise avoid it.
Common pitfall. Calling sizeof(arr)/sizeof(arr[0]) inside a function to get the length. Inside the function arr is already a pointer, so this computes 8 / 4 == 2 regardless of the real size.
Knowledge check (find the bug): A function receives
int scores[]and doesint n = sizeof(scores)/sizeof(scores[0]);. Why isnwrong? (Answer:scoresdecayed toint*;sizeof(scores)is the pointer size, not the array size, sonis meaningless — usually 2 on a 64-bit machine.)
Because of decay, these three parameter declarations are exactly equivalent:
void f(int a[10]); // the 10 is ignored by the compiler
void f(int a[]); // same
void f(int *a); // same — this is the truth of it
The caller passes an address; the callee receives a pointer. The length is not transmitted, so you pass it as a separate parameter (int n). This is why nearly every array-taking C function has a (pointer, length) signature.
Knowledge check (explain in your own words): Why must you pass the length separately when you pass an array to a function? (Because the array decays to a bare pointer, which carries an address but no size information.)
int a[5] = {10, 20, 30, 40, 50};
int x = a[2]; // subscript: same as *(a + 2) -> 30
int y = *(a + 2); // explicit pointer math -> 30
int *p = a; // decay: p now holds &a[0]
int z = p[2]; // pointers subscript too -> 30
size_t bytes = sizeof(a); // 20: whole array, NO decay
size_t count = sizeof(a)/sizeof(a[0]); // 5: valid ONLY where a is declared
int (*whole)[5] = &a; // &a does NOT decay: pointer-to-array-of-5
Key points: subscripting works on any pointer, not just arrays. The sizeof-based count trick is valid only in the scope where the array is declared, never after it has been passed to a function.
In most contexts, the name of an array decays to a pointer to its first element. "Decay" here just means the array name is automatically treated as the address of element 0.
Because of this, a[i] is exactly the same as *(a + i).
a + i is the address of element i.*(a + i) reads the value stored there.This equivalence is also why a function that takes an array can declare the parameter either way:
int a[]int *aBoth compile to the same thing.
Decay does not happen in every case. Watch out for these two:
sizeof on a declared array gives the total size of the array in bytes. But if the array was passed into a function (where it has decayed to a pointer), sizeof returns the size of the pointer instead.&a (the address of the array) has a different type from a (a pointer to the first element). They point to the same place, but the compiler treats them as different types.#include <stdio.h> #include <stddef.h>
/* Sum n elements by walking a pointer from a to a + n. The array has already decayed to int* here, so we MUST receive the length separately. */ long sum_via_ptr(const int *a, size_t n) { long total = 0; for (const int *p = a; p < a + n; ++p) { total += p; / read the element p points at */ } return total; }
int main(void) { int scores[] = {10, 20, 30, 40, 50};
/* sizeof works here because 'scores' is DECLARED in this scope. */
size_t n = sizeof(scores) / sizeof(scores[0]);
printf("element count: %zu\n", n);
printf("sizeof(scores) = %zu bytes\n", sizeof(scores));
/* Prove a[i] == *(a + i) for every element. */
for (size_t i = 0; i < n; ++i) {
printf("scores[%zu]=%d *(scores+%zu)=%d same=%s\n",
i, scores[i], i, *(scores + i),
scores[i] == *(scores + i) ? "yes" : "no");
}
/* Decay in action: passing the array yields a pointer. */
long s = sum_via_ptr(scores, n);
printf("sum = %ld\n", s);
return 0;
}
/* Expected output: element count: 5 sizeof(scores) = 20 bytes scores[0]=10 *(scores+0)=10 same=yes scores[1]=20 *(scores+1)=20 same=yes scores[2]=30 *(scores+2)=30 same=yes scores[3]=40 *(scores+3)=40 same=yes scores[4]=50 *(scores+4)=50 same=yes sum = 150
Edge cases: passing n = 0 makes the loop run zero times and returns 0, which is correct. Passing an n larger than the real array is undefined behaviour (out-of-bounds read). */
sum_via_ptr(const int *a, size_t n) — the parameter is written int *a, not int a[], to make the decay explicit: what actually arrives is a pointer. const promises we only read. n carries the length because the pointer cannot.for (const int *p = a; p < a + n; ++p) — p starts at the first element. The bound a + n is the one-past-the-end address (legal to compute, not to dereference). ++p advances by one int (pointer arithmetic scales by element size).total += *p; — *p reads the current element. Over the loop, p visits a, a+1, ... a+n-1.main, int scores[] = {10,...,50}; declares an array of 5 ints — 20 bytes contiguous.sizeof(scores) / sizeof(scores[0]) — here scores is a declared array, so sizeof sees all 20 bytes; dividing by 4 gives 5. This line would break inside any function that received scores as a parameter.scores[i] next to *(scores + i) and compares them — they match for every i, demonstrating the equivalence.sum_via_ptr(scores, n) — passing scores decays it to &scores[0]; the function receives that address plus the count.Trace of sum_via_ptr for {10,20,30,40,50}, n = 5:
| step | p points at |
*p |
total after |
|---|---|---|---|
| 1 | element 0 | 10 | 10 |
| 2 | element 1 | 20 | 30 |
| 3 | element 2 | 30 | 60 |
| 4 | element 3 | 40 | 100 |
| 5 | element 4 | 50 | 150 |
| stop | p == a + 5 |
— | returns 150 |
1. Counting elements with sizeof inside a function.
/* WRONG */
long sum(int a[], int unused) {
int n = sizeof(a) / sizeof(a[0]); /* a is int*, so this is 8/4 = 2 */
long t = 0;
for (int i = 0; i < n; i++) t += a[i]; /* only sums 2 elements! */
return t;
}
Why it is wrong: a decayed to a pointer, so sizeof(a) is the pointer size, not the array size. Fix: pass the length in.
/* CORRECT */
long sum(const int *a, size_t n) {
long t = 0;
for (size_t i = 0; i < n; i++) t += a[i];
return t;
}
How to catch it: if you compute a count from sizeof anywhere except the scope where the array is declared, treat it as a bug.
2. Confusing &a with a.
int a[5];
int *p = a; /* fine: points at a[0] */
int *q = &a; /* WRONG type: &a is int(*)[5], not int* */
Why it is wrong: &a and a point to the same byte but have different types; &a + 1 skips the whole array. Fix: use a (or &a[0]) when you want int*. Recognise it by the compiler warning "initialization from incompatible pointer type."
3. Treating a 2D array parameter as int **.
void f(int **m); /* WRONG for a real 2D array */
void f(int m[][4]); /* CORRECT: rows of 4 */
Why it is wrong: int grid[3][4] is 12 contiguous ints, not an array of pointers. int** assumes a pointer table that does not exist, so indexing reads garbage addresses. Fix: declare the column count so the compiler can compute row offsets.
Compiler errors / warnings.
initialization from incompatible pointer type when assigning &a to an int* — you meant a or &a[0].passing argument ... from incompatible pointer type when passing int(*)[N] where int* is expected. Turn on -Wall -Wextra; the compiler often flags decay mistakes for you.Runtime errors.
<= instead of <. Run under a sanitizer: gcc -fsanitize=address,undefined -g file.c reports out-of-bounds access with the exact line.Logic errors.
2 (on 64-bit) or 1 almost always means a sizeof/sizeof-inside-a-function bug: 8/4 = 2, 8/8 = 1.n) is off, or you decayed the array and lost the real length.Questions to ask when it doesn't work.
arr a declared array here, or a parameter that already decayed?< n (correct) or <= n (off-by-one overflow)?&a where I meant a?Arrays-as-pointers is where beginners first meet undefined behaviour, so treat these as hard rules.
0 .. n-1. a + n is a legal address to compute (one past the end) but illegal to dereference. Reading or writing a[n] (or beyond) is out-of-bounds — undefined behaviour, and the root of buffer-overflow vulnerabilities.n, and validate it: never trust a caller-supplied length that could exceed the real allocation.int a[5] = {0};) or write before you read.const correctness. If a function only reads, declare the parameter const int *. This lets the compiler stop accidental writes and documents intent.size_t for indices and counts (it is unsigned and sized for memory), not int, which can overflow.Defensive habit: compile with -Wall -Wextra -fsanitize=address,undefined during development. It catches most decay-related memory bugs at the exact moment they happen.
Concrete use case. Every C standard-library function that processes a buffer relies on decay and the (pointer, length) convention: memcpy(dst, src, n), qsort(base, count, size, cmp), and fread(buf, size, count, fp) all take a decayed pointer plus counts. When you call qsort(scores, n, sizeof scores[0], cmp), the array scores decays to a pointer exactly as in this lesson. Operating-system kernels, network stacks (parsing packet byte buffers), and embedded firmware (reading sensor sample arrays) all pass fixed buffers around this way because it avoids copying.
Professional best-practice habits.
Beginner:
sizeof(arr)/sizeof(arr[0]) only in the scope where the array is declared.< (not <=) as the loop bound.count, len, scores), not x/y.Advanced:
const on read-only pointer parameters and size_t for sizes and indices.-Wall -Wextra; run tests under AddressSanitizer/UBSan in CI.int (*p)[N] parameters so the compiler can enforce the length at the type level.Beginner 1 — Prove the equivalence.
Declare int a[6] with any values. Loop over the indices and print, for each i, both a[i] and *(a + i), plus whether they are equal. Requirement: use size_t for the index. Expected: every line reports equal. Concepts: decay, subscript vs. pointer math.
Beginner 2 — Length in the right place.
Write a main that declares an array, computes its element count with sizeof(arr)/sizeof(arr[0]), and prints it. Then pass the array to a function void show(const int *a, size_t n) that prints each element. Requirement: the count is computed in main, never inside show. Concepts: decay across a function boundary, passing length separately.
Intermediate 1 — Sum by walking a pointer.
Implement long sum_via_ptr(const int *a, size_t n) using a moving pointer p (no [] indexing at all). Input: {3, 1, 4, 1, 5}. Output: sum = 14. Constraint: loop condition must be p < a + n. Hint: initialise const int *p = a; and ++p. Concepts: pointer arithmetic, one-past-the-end bound.
Intermediate 2 — Find the max and its index.
Write int index_of_max(const int *a, size_t n) returning the index of the largest element (first one wins on ties). Input: {2, 9, 4, 9, 1}. Output: 2 (the first 9 is at index 1 — verify your tie rule). Constraint: return a sensible value for n == 0 and document it. Hint: track both the best value and its index. Concepts: bounds, indexing, edge cases.
Challenge — 2D sum without int**.
Write long grid_sum(int rows, int cols, int m[][/* cols */]) (choose a fixed column count, e.g. int m[][4]) that sums all elements of a 2D array. Requirement: do NOT use int**; explain in a comment why m[i][j] works via row-offset arithmetic (*(*(m + i) + j)). Input: a 3x4 grid of your choice. Output: the total. Hint: the compiler needs the column count to compute where row i starts. Concepts: 2D arrays, why the column dimension cannot decay away.
a[i] is defined as *(a + i) — same address, same value; [] is sugar over pointer arithmetic you already know.sizeof (gives the whole array's bytes) and &a (type pointer-to-array, not pointer-to-element).sizeof(arr)/sizeof(arr[0]) only works where the array is declared.sizeof inside a function, confusing &a with a, treating a 2D array as int**, and reading past the end (undefined behaviour / buffer overflow).0 .. n-1, use < for the loop bound, prefer const and size_t, and compile with -Wall -Wextra -fsanitize=address,undefined to catch bounds bugs early.