Two things that look alike but are not the same
By now you know what a pointer is — a value that holds an address — and from the previous guide you know that `*p` reads the box that address names, and that `p + 1` does pointer arithmetic, stepping forward by one whole element rather than one byte. Now we meet the other beginner-famous resident of memory: the array. The folklore says "an array is just a pointer," and you will hear it constantly. It is close enough to be useful and wrong enough to cost you an afternoon, so let us pin down exactly where the truth begins and ends.
Start with what an array actually is. When you write `int a[4];`, the compiler sets aside one contiguous block of memory big enough for four `int` values, side by side, with no gaps. There is no separate "array variable" hiding a pointer somewhere; the name `a` simply is those 16 bytes (four 4-byte ints). A pointer, by contrast, is its own little box — typically 8 bytes on a 64-bit machine — whose contents are an address. So `int a[4]` and `int *p` are different kinds of object: one is a row of houses, the other is a slip of paper with a street number written on it.
int a[4] = { 10, 20, 30, 40 };
name 'a' IS these 16 bytes, in place:
+--------+--------+--------+--------+
| 10 | 20 | 30 | 40 |
+--------+--------+--------+--------+
a[0] a[1] a[2] a[3]
0x1000 0x1004 0x1008 0x100c
int *p = a; // p is a SEPARATE 8-byte box living elsewhere:
+-----------------+
| 0x1000 | p holds the address of a[0]
+-----------------+Decay: where the legend comes from
If they are different kinds of thing, why does everyone confuse them? Because of one specific rule called array-to-pointer decay. In almost every expression where you use an array's name, C silently converts that name into a pointer to the array's first element. So in `int *p = a;`, the `a` on the right does not copy 16 bytes; it quietly becomes `&a[0]`, an `int *` holding the address of the first element. The name `a` "decayed" into a pointer the instant you used it in a value context. That single conversion is the entire grain of truth behind "arrays are pointers" — they are not, but they turn into one almost everywhere you look.
Decay is also why passing an array to a function feels like passing a pointer. C parameters are always passed by value, and you cannot pass a whole array by value — there is no rule for copying 16 bytes into a parameter. So when you call `f(a)`, the array decays first, and what the function actually receives is an `int *`. A declaration like `void f(int arr[])` is a polite lie: the compiler rewrites it to `void f(int *arr)`. Inside `f`, `arr` is a genuine pointer, not an array, which is why — as we are about to see — `sizeof` inside the function gives you the size of a pointer, not the size of the original array.
Why a[i] is secretly pointer arithmetic
Here is the most delightful piece of the truth. The square-bracket subscript you have used since your first loop is not a special array feature at all — it is plain pointer arithmetic in disguise. The language defines `a[i]` to mean exactly `*(a + i)`: take the address, step forward `i` elements, then dereference. This is indexing as pointer arithmetic, and once you see it you cannot unsee it. To read `a[2]`, the machine takes the base address (say 0x1000), adds 2 elements of 4 bytes each to reach 0x1008, and reads the `int` there. The brackets are just sugar over the addition and the `*`.
Because subscripting is literally `*(a + i)`, and ordinary addition does not care about the order of its operands, `a + i` equals `i + a`, which means `a[i]` and `i[a]` are the same expression. Yes — `3[a]` is legal C and reads `a[3]`. Nobody writes that on purpose, but it is the cleanest possible proof that the brackets are arithmetic and nothing more. The element type matters here: the compiler scales `i` by `sizeof(element)` automatically, so the same `a[i]` notation works whether each slot is a 1-byte `char`, a 4-byte `int`, or a 40-byte `struct` — the scaling is invisible but always present.
This unification is also why a pointer and an array index the same way. If `int *p = a;`, then `p[2]` works identically to `a[2]`, because `p[2]` is `*(p + 2)` and `p` already holds the base address. So in most code you really can use the two interchangeably — and that is the legend doing its useful work. The trap is believing the interchangeability is total. It is not, and the next section is where it stops.
Where an array refuses to be a pointer
There are a few specific places where the name does not decay, and the array shows its true nature. The most important is the sizeof operator. Applied to a real array, `sizeof(a)` gives the size of the whole block — for our `int a[4]` that is 16 bytes — and a common idiom computes the element count as `sizeof(a) / sizeof(a[0])`, which gives 4. But apply `sizeof` to a pointer, including the `arr` parameter inside `f(int arr[])`, and you get the size of the pointer itself, usually 8, no matter how big the original array was. This is the single most common way the "arrays are pointers" myth bites: the count idiom silently returns 8/4 = 2 inside the function, and a loop that should visit 4 elements visits 2.
The practical consequence: a function that receives an array cannot recover its length from the pointer alone — the length information was lost at the moment of decay. That is why nearly every C function that takes an array also takes a separate count, like `void sum(int *arr, size_t n)`. There is no hidden header storing the length the way higher-level languages provide; you must carry the count yourself. The other non-decay spots are narrower but worth knowing: `&a` gives a pointer to the whole array (type `int (*)[4]`, a different type from `int *`), and an array name is not an assignable, modifiable value — you cannot write `a = something;` because `a` is not a variable that holds a rebindable address, it is the storage itself.
Strings, walking the edge, and the honest caveats
All of this comes together in C's strings, which are nothing but `char` arrays with a convention. A null-terminated string is a run of characters ending in a `\0` byte (value 0x00), and a function like a hand-written `strlen()` works precisely because it is array indexing as pointer arithmetic: start a `char *` at the front and walk it forward one byte at a time — `while (*p != '\0') p++;` — counting until it lands on the terminator. There is no stored length; the `\0` is the length marker. This is elegant and also exactly why a missing terminator sends the walk off the end of the array into memory it does not own.
Which brings the honest warning this whole guide has been building toward. Pointer arithmetic into an array is only defined as long as you stay inside the array, plus the one address just past its last element (you may compute that one-past-the-end pointer, but you must not dereference it). Step further — read `a[4]` in our four-element array, or run a string walk past a missing `\0` — and you have committed an out-of-bounds access, which is undefined behavior. Be exact about what that means: it is not "you read some random neighbouring value." The standard places no constraints at all on what the program may do, and a modern optimizer is allowed to assume the access never happens, which is why an out-of-bounds bug can appear harmless at `-O0` and silently corrupt or crash at `-O2`.
So hold both halves of the truth at once. The useful half: arrays decay into pointers nearly everywhere, `a[i]` is `*(a + i)`, and you can pass and walk them with a plain `char *` or `int *`. The dangerous half: an array is not a pointer, it carries no length, and the bounds are yours alone to guard. None of this is "simple once you get it" — off-by-one walks past the end are among the most common and most exploited bugs in all of C. The next two guides face the consequences head-on: where these arrays actually live (the stack frame), and what goes wrong when a pointer outlives the memory it names (dangling pointers and the segfault).