This page is a shooting range . The parent note taught you the rule; here we fire every kind of situation at that rule and watch what happens — normal arrays, empty-looking ones, the three no-decay exceptions, char[] strings, 2-D arrays, and the exam trick questions. When you finish, no scenario should surprise you.
For every example below we assume a typical machine where int is 4 bytes and a pointer is 8 bytes (64-bit). If your machine differs, only the numbers scale — the reasoning is identical.
Every case this topic can throw at you falls into one of these cells. Each worked example is tagged with the cell(s) it covers.
#
Cell class
What makes it tricky
Covered by
A
Plain decay in a value expression
array name → T* silently
Ex 1
B
sizeof exception (no decay)
you get whole-array bytes
Ex 1, Ex 2
C
&arr exception (no decay)
type becomes T(*)[N], +1 jumps far
Ex 3
D
Passing to a function (size lost)
int a[] param is really int*
Ex 4
E
arr[i] == *(arr+i) == i[arr] identity
commutativity of +
Ex 5
F
2-D / multidimensional decay
only the outermost dimension peels off
Ex 6
G
char[] vs char* string literal
initialiser is the 3rd exception
Ex 7
H
Degenerate / edge inputs
1-element array, sizeof of a param, &arr[0] vs arr
Ex 8
I
Real-world word problem
sum a sensor buffer safely
Ex 9
J
Exam-style twist
sizeof(a)/sizeof(a[0]) element count trick
Ex 10
We will now hit every cell.
Worked example 1 — Plain decay vs
sizeof (cells A, B)
int a [ 10 ];
int * p = a; // (1)
printf ( " %zu\n " , sizeof (a)); // (2)
printf ( " %zu\n " , sizeof (p)); // (3)
Forecast: guess the two printed numbers before reading on.
Step 1 — int *p = a;
Why this step? Assignment is a value context , so a decays to &a[0] of type int*. p now holds the base address. WHAT it looks like: an arrow pointing at box #0 of the row.
Step 2 — sizeof(a)
Why this step? sizeof is exception #1 — the name does not decay. It sees the full type int[10], so it reports 10 × 4 = 40 bytes.
Step 3 — sizeof(p)
Why this step? p is a genuine pointer variable, so you get the pointer width: 8 .
Verify: 10 × 4 = 40 ✓, pointer width = 8 ✓. Different numbers = proof that an array is not a pointer.
sizeof on a bigger element type (cell B, edge on sizeof(T))
double d [ 6 ];
printf ( " %zu\n " , sizeof (d)); // (1)
printf ( " %zu\n " , sizeof ( d [ 0 ])); // (2)
printf ( " %zu\n " , sizeof (d) /sizeof ( d [ 0 ])); // (3)
Forecast: with double = 8 bytes, guess all three.
Step 1 — sizeof(d)
Why this step? No decay (exception). Whole array of 6 doubles: 6 × 8 = 48 .
Step 2 — sizeof(d[0])
Why this step? d[0] is one element of type double: 8 .
Step 3 — the ratio
Why this step? 48/8 = 6 recovers the element count . This trick only works where the array is declared (no decay yet).
Verify: 6 × 8 = 48 ✓, 48/8 = 6 = the declared length ✓.
&arr steps by the whole array (cell C)
int a [ 5 ];
int * p = a; // int* -> a[0]
int ( * q)[ 5 ] = & a; // int(*)[5] -> whole array
// numeric addresses: p == q
// p+1 vs q+1 ?
Forecast: if the base address is 1000 , what are p+1 and q+1?
Step 1 — p decays.
Why this step? int *p = a; → a decays, p points at a[0], type int*.
Step 2 — &a does NOT decay.
Why this step? Exception #2. &a has type int(*)[5] — a pointer to the whole 5-int row . Same numeric value as p, different type.
Step 3 — pointer arithmetic scales by the pointed-to size.
Why this step? Adding 1 to int* steps sizeof(int) = 4 bytes ⇒ p+1 = 1004. Adding 1 to int(*)[5] steps sizeof(int[5]) = 20 bytes ⇒ q+1 = 1020.
Verify: 1000 + 4 = 1004 ✓, 1000 + 5 × 4 = 1020 ✓. Same start, different stride — the type decides the step.
Worked example 4 — The size is lost across a call (cell D)
void f ( int arr [ 100 ]) { // rewritten to int *arr
printf ( " %zu\n " , sizeof (arr)); // (1)
}
int main ( void ){ int a [ 100 ]; f (a); }
Forecast: does the 100 inside the brackets protect us? Guess the print.
Step 1 — the parameter is a lie.
Why this step? C ignores the 100: any array parameter int arr[...] is rewritten to int *arr. The size annotation is decoration only.
Step 2 — at the call f(a), a decays.
Why this step? Value context → a becomes &a[0]. The function receives only an 8-byte address; it never learns the length.
Step 3 — sizeof(arr) inside f.
Why this step? arr is genuinely a pointer here ⇒ 8 , not 400.
Verify: printed value = pointer width = 8 ✓ (NOT 100 × 4 = 400 ). Rule: only sizeof an array in the scope where it is declared (that's why Ex 2 worked and this doesn't).
arr[i] == *(arr+i) == i[arr] (cell E)
int a [ 4 ] = { 10 , 20 , 30 , 40 };
// all four print the same value:
printf ( " %d %d %d %d\n " , a [ 2 ], * (a + 2 ), * ( 2 + a), 2 [a]);
Forecast: which of these is illegal? (Trick: none are.)
Step 1 — the definition.
Why this step? The C standard defines a[i] as *(a + i). Here a decays to &a[0].
Step 2 — commutativity.
Why this step? Addition commutes: a + 2 == 2 + a. So *(a+2) == *(2+a).
Step 3 — un-sugar the second form.
Why this step? 2[a] is defined as *(2 + a) — the compiler applies the same rule with operands swapped. Therefore 2[a] == a[2].
Verify: all four expressions read the element at offset 2 = 30 ✓. Weird syntax, one arithmetic.
Worked example 6 — 2-D decay peels one layer (cell F)
int m [ 3 ][ 4 ];
// type of m when used as value?
// sizeof(m), sizeof(m[0]), sizeof(m[0][0]) ?
Forecast: does m decay to int**? (No! Guess the real type.)
Step 1 — what m decays to.
Why this step? m is an array of 3 rows , each row being int[4]. Decay peels only the outermost dimension: m → pointer to its first element, and its first element is int[4]. So m → int(*)[4], not int**.
Step 2 — sizeof(m) (no decay).
Why this step? Whole 2-D block: 3 × 4 × 4 = 48 bytes.
Step 3 — sizeof(m[0]) and sizeof(m[0][0]).
Why this step? m[0] is one row int[4] ⇒ 4 × 4 = 16 . m[0][0] is one int ⇒ 4.
Verify: 48/16 = 3 rows ✓, 16/4 = 4 columns ✓, and m+1 steps 16 bytes (a whole row) because m is int(*)[4] ✓.
char[] vs char* and the literal exception (cell G)
char s [] = "hi" ; // (1) array of 3 chars: 'h','i','\0'
char * t = "hi" ; // (2) pointer to a read-only literal
printf ( " %zu %zu\n " , sizeof (s), sizeof (t)); // (3)
Forecast: how many bytes does "hi" really occupy, and what are the two sizeofs?
Step 1 — char s[] = "hi";
Why this step? Exception #3: a string literal initialising a char[] does not decay — its characters are copied into s. s holds 'h', 'i', and the terminating '\0' ⇒ 3 bytes.
Step 2 — char *t = "hi";
Why this step? Here the literal is a value , so it decays to a char* pointing at read-only storage. t is an 8-byte pointer; writing through it is undefined behaviour.
Step 3 — the two sizeofs.
Why this step? sizeof(s) = whole array = 3 (no decay). sizeof(t) = pointer width = 8 .
Verify: "hi" = 2 visible chars + 1 null = 3 ✓; sizeof(s)=3 ✓, sizeof(t)=8 ✓.
Worked example 8 — Degenerate inputs: 1-element array and
arr vs &arr[0] (cell H)
int one [ 1 ] = { 42 };
printf ( " %zu\n " , sizeof (one)); // (1)
printf ( " %d\n " , one == & one [ 0 ]); // (2) same value?
printf ( " %d\n " , ( void* ) & one == ( void* ) one ); // (3) same address?
Forecast: even with a single box, does decay still happen? Guess all three prints.
Step 1 — sizeof(one).
Why this step? No decay in sizeof, even for length 1: 1 × 4 = 4 .
Step 2 — one == &one[0].
Why this step? In this comparison one decays to &one[0]. Both sides are the same int* value ⇒ comparison is 1 (true).
Step 3 — &one numeric value.
Why this step? &one (type int(*)[1]) has the same numeric address as one, just a different type. Cast both to void* ⇒ equal ⇒ 1 .
Verify: 1 × 4 = 4 ✓, one == &one[0] = 1 ✓, address equality = 1 ✓. Edge case behaves exactly like the general rule.
Worked example 9 — Real-world: sum a sensor buffer safely (cell I)
A temperature logger fills int temps[8] = {21,22,20,23,25,24,22,21}. You must pass it to int sum(int *buf, int n) and total it. Why can't sum just call sizeof?
Forecast: what length must you pass, and what is the total?
Step 1 — compute the length where the array is declared.
Why this step? Only in main (declaration scope) does sizeof(temps)/sizeof(temps[0]) work — it gives 32/4 = 8 . Inside sum, buf is int*, so sizeof there would give 8 bytes (pointer), useless.
Step 2 — call sum(temps, 8).
Why this step? temps decays to &temps[0]; the function walks buf[0..7] using pointer arithmetic. The length must ride along separately because decay erased it.
Step 3 — add them up.
Why this step? 21 + 22 + 20 + 23 + 25 + 24 + 22 + 21 .
Verify: length = 32/4 = 8 ✓; sum = 178 ✓ (units: °C·count is meaningless as a physical unit — this is a raw total, mean would be 178/8 = 22.25 °C).
Worked example 10 — Exam twist: the element-count trap (cell J)
int a [ 10 ];
// Question: does this print 10 in BOTH places?
int n1 = sizeof (a) /sizeof ( a [ 0 ]); // in main
int g ( int a [] ) { return sizeof (a) /sizeof ( a [ 0 ]); } // in a function
Forecast: the classic exam gotcha — do n1 and g(a) agree?
Step 1 — n1 in main.
Why this step? No decay here: 4 40 = 10 . Correct count.
Step 2 — inside g.
Why this step? a is really int*. sizeof(a) = 8 (pointer), sizeof(a[0]) = 4 (one int). Ratio = 8/4 = 2 — wrong , and machine-dependent!
Step 3 — the lesson.
Why this step? The sizeof/sizeof idiom is valid only in declaration scope. Across a call, decay destroyed the size, so the idiom silently returns garbage (here 2).
Verify: 40/4 = 10 ✓ (main), 8/4 = 2 ✓ (function, the trap). They do not agree — that is the whole point of the exam question.
Recall Quick self-test over the whole matrix
sizeof(a) for int a[10]? ::: 40 (no decay, whole array).
sizeof(p) for int *p = a;? ::: 8 (pointer width).
Type m decays to for int m[3][4]? ::: int(*)[4], not int**.
sizeof(m) for int m[3][4]? ::: 48 bytes (3 × 4 × 4 ).
sizeof(s) for char s[]="hi";? ::: 3 (h, i, \0).
sizeof(t) for char *t="hi";? ::: 8 (pointer).
Element count of int a[10] inside g(int a[])? ::: 2 on a 64-bit machine (8/4) — the trap; decay lost the size.
q+1 for int(*q)[5] at base 1000? ::: 1020 (steps 20 bytes = whole 5-int array).
Sum of {21,22,20,23,25,24,22,21}? ::: 178.
Declare-scope sizeof = truth; anywhere else = 8. If you can see the [N] in the same function, sizeof counts real bytes. Once it crossed a call, only int* survived.
Pointer Arithmetic — why p+1 and q+1 step different byte distances (Ex 3, 6).
Passing Arrays to Functions — the lost-size fallout (Ex 4, 9, 10).
sizeof Operator — the #1 no-decay exception, used in every example.
Multidimensional Arrays — decay peels only the outer dimension (Ex 6).
Strings in C — char[] vs char* and literal decay (Ex 7).
Memory Layout and Addresses — contiguous boxes underlie every stride computation.