The Analogy: A Street of Mailboxes
Imagine a street with mailboxes in a perfect row, each with a house number painted on it โ 1000, 1004, 1008, 1012, 1016. That's exactly what an array is in your computer's memory: a row of storage slots, each with its own address, sitting right next to each other with no gaps.
When you write int arr[5] = {10, 20, 30, 40, 50};, C finds 5 mailboxes in a row and puts your numbers inside them.
Now here's the part that unlocks everything else: the name arr itself is just the address of the first mailbox โ 1000. It isn't a box that contains 1000; it IS 1000, painted permanently on the street sign at the very start of the row. You can't repaint that sign. You can't move the row. That's the whole reason certain things you'd expect to work on arrays actually don't.
A pointer, on the other hand, is a completely different kind of thing: it's a small note you carry in your pocket, and on that note you write down whatever address you want โ 1000, 1008, or anywhere else in the city. You can scribble out the old number and write a new one any time.
Proof: arr Is Already an Address
Don't take the analogy on faith โ let's ask C directly. If arr is really the address of the first mailbox, then printing arr and printing &arr[0] (the "address of the box at index 0") should show the exact same number.
#include <stdio.h> int main() { int arr[5] = {10, 20, 30, 40, 50}; printf("arr = %p\n", (void*)arr); printf("&arr[0] = %p\n", (void*)&arr[0]); return 0; }
arr = 0x7ffee2a1c9c0 &arr[0] = 0x7ffee2a1c9c0
Identical. Not similar โ identical. That's not a coincidence built into this one example; it's true for every array, every time. arr was never "a box holding an address" the way a pointer is โ writing arr in your code always means "the address where this array starts," full stop.
A Pointer Walking Down the Row
Now let's actually use a pointer as the "note in your pocket." We copy arr's address onto the note (ptr = arr;), then rewrite the note to point one mailbox further each time with ptr++.
#include <stdio.h> int main() { int arr[5] = {10, 20, 30, 40, 50}; int *ptr = arr; // copy the address onto the "note" for (int i = 0; i < 5; i++) { printf("ptr = %p, *ptr = %d\n", (void*)ptr, *ptr); ptr++; // rewrite the note to the next mailbox } return 0; }
ptr = 0x7ffee2a1c9c0, *ptr = 10 ptr = 0x7ffee2a1c9c4, *ptr = 20 ptr = 0x7ffee2a1c9c8, *ptr = 30 ptr = 0x7ffee2a1c9cc, *ptr = 40 ptr = 0x7ffee2a1c9d0, *ptr = 50
ptr++ doesn't mean "add 1 to the number" โ it means "move to the next mailbox," and since each mailbox here holds an int (4 bytes), the address jumps by 4. A char* would jump by 1, a double* by 8. The pointer's type tells C how big one step is.arr[i] Is Secretly *(arr + i)
Here's the reveal: square-bracket indexing isn't its own separate feature of C. arr[i] is just shorthand that the compiler expands into *(arr + i) โ "walk i mailboxes down from the start, then open that box." That's it. That's the entire trick behind array indexing.
#include <stdio.h> int main() { int arr[5] = {10, 20, 30, 40, 50}; for (int i = 0; i < 5; i++) { printf("arr[%d] = %d | *(arr+%d) = %d\n", i, arr[i], i, *(arr + i)); } // Fun (but confusing) proof: C doesn't care which side the number is on printf("\narr[2] = %d\n", arr[2]); printf("2[arr] = %d\n", 2[arr]); // yes, this really compiles return 0; }
arr[0] = 10 | *(arr+0) = 10 arr[1] = 20 | *(arr+1) = 20 arr[2] = 30 | *(arr+2) = 30 arr[3] = 40 | *(arr+3) = 40 arr[4] = 50 | *(arr+4) = 50 arr[2] = 30 2[arr] = 30
2[arr] looks like a typo, but it compiles and works, because arr[2] was never anything more than *(arr + 2) โ and addition doesn't care about order, so *(arr + 2) and *(2 + arr) are the same thing. Nobody writes code this way, but seeing it work proves the rule isn't a metaphor โ it's literally how the compiler treats [ ].
Why arr++ Doesn't Compile (But ptr++ Does)
This is where the analogy earns its keep. ptr is a note in your pocket โ of course you can cross out the number and write a new one, that's what pockets and notes are for. But arr is a street sign painted into the layout of memory itself. There's no "pocket" to put a new number into โ asking to change arr is like asking to repaint the fixed starting address of the row. C simply won't allow it.
#include <stdio.h> int main() { int arr[3] = {1, 2, 3}; int *ptr = arr; ptr++; // fine โ the note can be rewritten printf("*ptr after ptr++ = %d\n", *ptr); // arr++; // COMPILE ERROR if uncommented: // "lvalue required as increment operand" // arr is not a variable holding an address โ it IS the address. // There's no storage location for "arr" to update. printf("arr[0] is still = %d\n", arr[0]); return 0; }
*ptr after ptr++ = 2 arr[0] is still = 1
| Array name (arr) | Pointer variable (ptr) | |
|---|---|---|
| What it is | A fixed address, baked into memory layout | A variable that stores an address |
| Can you reassign it? | No โ arr++ is a compile error | Yes โ ptr++, ptr = &x, anything |
| Takes up its own memory? | No extra memory โ it's not a "box," it's a label | Yes โ the pointer itself lives somewhere and needs storage |
| sizeof it | Size of the whole array (see next section) | Size of one address (8 bytes on a 64-bit system) |
The sizeof Trap โ Where the Analogy Saves You
An array name isn't just an address โ the compiler also remembers how many mailboxes are in the row, because it laid them out itself. A pointer only ever knows one address; it has no idea how many boxes come after it. This difference explains a bug almost every C beginner hits at least once.
#include <stdio.h> void printSize(int arr[]) { // secretly becomes int* arr โ it decays! printf("Inside function : sizeof(arr) = %zu bytes\n", sizeof(arr)); } int main() { int arr[5] = {10, 20, 30, 40, 50}; int *ptr = arr; printf("In main, sizeof(arr) = %zu bytes (5 ints x 4 bytes)\n", sizeof(arr)); printf("In main, sizeof(ptr) = %zu bytes (just one address)\n", sizeof(ptr)); printSize(arr); // arr "decays" into a plain int* the moment it's passed return 0; }
In main, sizeof(arr) = 20 bytes (5 ints x 4 bytes) In main, sizeof(ptr) = 8 bytes (just one address) Inside function : sizeof(arr) = 8 bytes
sizeof(arr) inside printSize() reports 8 (one address), not 20. This is exactly why array-processing functions in C always take a separate length parameter โ void printSize(int arr[], int length) โ the function has no other way to know how many boxes to walk through.char* and int* step differently โ is just this one mailbox picture applied in different situations. There was never a separate "array world" and "pointer world"; arrays are what memory actually looks like, and pointers are the tool C gives you to talk about addresses in that memory.Quick Quiz
What does the array name arr actually represent?
arr[i] is shorthand for which expression?
Why does arr++ fail to compile, while ptr++ works fine?
If int arr[10], what does sizeof(arr) return inside the same function it was declared in?
Why must array-processing functions take a separate length parameter?
Lesson Checklist
- I can explain memory as a "row of mailboxes" in my own words
- I understand arr and &arr[0] are literally the same address
- I can explain why ptr++ moves by the size of the data type, not by 1
- I understand arr[i] is really *(arr + i)
- I can explain why arr++ doesn't compile but ptr++ does
- I know sizeof(arr) gives total bytes, but decays to pointer size in a function
- I understand why array functions need a length parameter
- I completed the quiz