Intuition What this page is
The parent note gave you the machinery: row-major storage, the address formula addr = base + ( i C + j ) ⋅ s , and the pointer identity a [ i ] [ j ] ≡ ∗ ( ∗ ( a + i ) + j ) . Here we stress-test that machinery against every kind of input a 2D/3D array can throw at you — corner elements, the very first and very last box, degenerate one-row arrays, the pointer-type traps, and an exam twist. Guess each answer before you read the steps.
Every worked example below is tagged with a cell so you can see the coverage is complete.
Cell
Case class
What makes it tricky
Example
A
Interior element a[i][j], both indices > 0
plain formula
Ex 1
B
First element a[0][0]
zero offset — does base survive?
Ex 2
C
Last element a[R-1][C-1]
biggest offset, must equal size−1 element
Ex 2
D
Degenerate: single row a[1][N] or single column a[N][1]
"is it still 2D?"
Ex 3
E
3D element a[i][j][k]
product-to-the-right rule
Ex 4
F
Pointer arithmetic reading (a+i vs *a+i)
row-jump vs element-jump
Ex 5
G
Type/decay twist: sizeof on a, a[i], a[i][j]
which dimension survives decay
Ex 6
H
Real-world word problem
modelling a grid
Ex 7
I
Exam trap: a[1,2] comma operator
language gotcha
Ex 8
J
Boundary: reading one-past-the-row (a[0][C] aliases a[1][0])
limiting/overflow index
Ex 9
Ten cells, nine examples (Ex 2 covers both B and C — the extremes of one array). Let's go.
Worked example Ex 1 — Cell A: an ordinary interior element
int a[4][5]; base address = 2000 , sizeof(int) = 4. Find &a[2][3].
Forecast: guess the byte offset from base before reading. (Hint: it's more than 20, less than 80.)
Read off the numbers. i = 2 , j = 3 , C = 5 (columns), s = 4 bytes.
Why this step? The formula only ever uses C , i , j , s — never R (rows). Pull those four out first so you never reach for the wrong number.
Count elements skipped. i C + j = 2 ⋅ 5 + 3 = 13 .
Why this step? Row-major means whole rows sit back-to-back; skipping 2 full rows is 2 × 5 = 10 elements, then 3 more inside row 2. Look at the shelf figure below — you literally walk 13 boxes.
Convert to bytes. 13 × 4 = 52 .
Why this step? An address is a byte count; each int occupies s = 4 bytes.
Add base. 2000 + 52 = 2052 .
Verify: the element after a[2][3] is a[2][4] at offset ( 2 ⋅ 5 + 4 ) ⋅ 4 = 56 , i.e. 2056 — exactly 4 bytes further. One int apart. ✅
Worked example Ex 2 — Cells B & C: the two extremes of one array
Same int a[4][5], base 2000 , sizeof(int)=4. Find (B) &a[0][0] and (C) &a[3][4] (the very last element).
Forecast: the first should land exactly on base ; the last should be one element shy of the whole block's end.
(B) First element. i = 0 , j = 0 ⇒ i C + j = 0 . Offset 0 ⋅ 4 = 0 . Address = 2000 .
Why this step? a[0][0] is the start of the flat block, so its address is the base. This is why &a[0][0] == (int*)a is always true.
(C) Last element. i = 3 , j = 4 ⇒ i C + j = 3 ⋅ 5 + 4 = 19 .
Why this step? The array has 4 × 5 = 20 elements, indexed 0 … 19 . The last one sits at element-index 19 — the biggest legal offset.
Bytes for (C). 19 × 4 = 76 . Address = 2000 + 76 = 2076 .
Why this step? Convert element count to bytes, add base.
Verify: whole-array size is 20 × 4 = 80 bytes, occupying addresses 2000 … 2079 . Last element starts at 2076 and ends at 2079 — it fills the final 4 bytes exactly. ✅ And 2076 − 2000 = 76 = ( elements − 1 ) × s . ✅
Worked example Ex 3 — Cell D: degenerate single-row and single-column arrays
int r[1][6]; and int c[6][1]; — both hold 6 ints. For r, find offset of r[0][5]. For c, find offset of c[5][0]. (base 0, sizeof(int)=4)
Forecast: both are secretly just a flat line of 6 ints — do you expect the same offset for the last element?
r[0][5]: here C = 6 . Offset = ( 0 ⋅ 6 + 5 ) ⋅ 4 = 20 .
Why this step? With one row, the row-skip term i C vanishes (i = 0 ), so we only walk columns.
c[5][0]: here C = 1 . Offset = ( 5 ⋅ 1 + 0 ) ⋅ 4 = 20 .
Why this step? With one column, each "row" is just a single int, so skipping 5 rows is skipping 5 ints.
Compare. Both give 20 .
Why this step? Both are 6 contiguous ints; the last element is 5 elements past the start either way. The grid shape is a mental fiction over the same flat memory.
Verify: total elements = 6 in each, so the last element sits at offset ( 6 − 1 ) ⋅ 4 = 20 . Matches both. ✅ A "1D-in-disguise" array behaves like a 1D array of the same length.
Worked example Ex 4 — Cell E: the 3D "product to the right" rule
int a[2][3][4]; base = 5000 , sizeof(int)=4. Find &a[1][2][3] (the last element).
Forecast: it's the last of 2 × 3 × 4 = 24 ints — where does the offset land?
Assign the multipliers. For a[X][Y][Z] = a[2][3][4]: index i is multiplied by Y ⋅ Z = 3 ⋅ 4 = 12 ; index j by Z = 4 ; index k by 1 .
Why this step? Each index steps over "everything to its right". One plane is a [ 3 ] [ 4 ] block = 12 ints; one row inside a plane is 4 ints; one element is 1 .
Plug in. i ⋅ 12 + j ⋅ 4 + k = 1 ⋅ 12 + 2 ⋅ 4 + 3 = 12 + 8 + 3 = 23 .
Why this step? This is the generalized i C + j — now with three nested layers.
Bytes + base. 23 × 4 = 92 ; 5000 + 92 = 5092 .
Verify: 2 × 3 × 4 = 24 ints, indices 0 … 23 ; the last element sits at element-index 23 . ✅ It occupies bytes 5092 … 5095 ; the block ends at 5000 + 24 ⋅ 4 − 1 = 5095 . ✅
Worked example Ex 5 — Cell F:
a+i (jump a row) vs *a + i (jump an element)
int a[3][4]; stored at base 1000 , sizeof(int)=4. Give the byte address that (a) a + 1 points to, and (b) *a + 1 (equivalently a[0] + 1) points to.
Forecast: one of these jumps far, the other jumps just one int. Which is which?
Type of a. a decays to int (*)[4] — a pointer to a row of 4 ints .
Why this step? Pointer arithmetic scales by the size of the pointed-to type . Here that type is int[4], which is 4 × 4 = 16 bytes.
(a) a + 1. Advances by 16 bytes: address = 1000 + 16 = 1016 .
Why this step? Adding 1 to a "pointer to a whole row" jumps one full row — that's a[1], i.e. &a[1][0].
Type of *a. *a is a[0], an int[4], which decays to int* — a pointer to a single int .
Why this step? One dereference peels off a dimension; now arithmetic scales by sizeof(int)=4.
(b) *a + 1. Advances by 4 bytes: address = 1000 + 4 = 1004 — that's &a[0][1].
Verify: &a[1][0] by the formula = 1000 + ( 1 ⋅ 4 + 0 ) ⋅ 4 = 1016 ✅ matches (a). &a[0][1] = 1000 + ( 0 ⋅ 4 + 1 ) ⋅ 4 = 1004 ✅ matches (b). Same pointer, one extra star, wildly different stride.
Worked example Ex 6 — Cell G:
sizeof on each level of decay
int a[3][4]; with sizeof(int)=4. What are sizeof(a), sizeof(a[0]), and sizeof(a[0][0])?
Forecast: guess all three in bytes before reading. (One of them is 48.)
sizeof(a). Whole array = 3 × 4 × 4 = 48 bytes.
Why this step? Inside sizeof, the array does not decay to a pointer — sizeof sees the full object. So you get every byte.
sizeof(a[0]). a[0] is one row, int[4] = 4 × 4 = 16 bytes.
Why this step? Again no decay under sizeof; a[0] is a genuine array of 4 ints, so its size is the whole row.
sizeof(a[0][0]). A single int = 4 bytes.
Why this step? Bottom of the ladder — one element.
Neat cross-check. sizeof ( a ) / sizeof ( a [ 0 ]) = 48/16 = 3 = number of rows; and sizeof ( a [ 0 ]) / sizeof ( a [ 0 ] [ 0 ]) = 16/4 = 4 = number of columns.
Why this step? This is the classic idiom for recovering dimensions at runtime without hard-coding them.
Verify: 48 = 3 ⋅ 16 = 12 ⋅ 4 . ✅ Row count 48/16 = 3 ✅, column count 16/4 = 4 ✅.
sizeof-after-passing trap
If you pass a to a function void f(int a[3][4]), inside f the parameter is really int (*)[4], so sizeof(a) there gives the pointer size (e.g. 8), not 48. sizeof only sees the full array in the scope where it was declared .
Worked example Ex 7 — Cell H: real-world word problem (a seating grid)
A cinema has 8 rows × 12 seats. Ticket price rises by ₹20 per row further back (front row = row 0 costs ₹100). Model it as int price[8][12] filled by price[i][j] = 100 + 20*i. What is the total revenue if every seat sells?
Forecast: all 12 seats in a row cost the same. Guess whether the answer is nearer ₹15,000 or ₹25,000.
Price per row. Row i price = 100 + 20 i . Rows 0 … 7 give ₹100, 120, 140, 160, 180, 200, 220, 240.
Why this step? Every column in a row shares one price, so we can sum rows and multiply by seats.
Sum the 8 row-prices. 100 + 120 + 140 + 160 + 180 + 200 + 220 + 240 = 1360 .
Why this step? This is ∑ i = 0 7 ( 100 + 20 i ) = 8 ⋅ 100 + 20 ⋅ ( 0 + 1 + ⋯ + 7 ) = 800 + 20 ⋅ 28 = 800 + 560 = 1360 .
Multiply by 12 seats per row. 1360 × 12 = 16320 .
Why this step? Each of those 8 row-prices is paid by 12 seats.
Verify: average price = 1360/8 = 170 ; total seats = 8 × 12 = 96 ; revenue = 170 × 96 = 16320 . ✅ Nearer ₹15,000, as forecast.
Worked example Ex 8 — Cell I: the
a[1,2] exam trap
int a[3][4] = { {0,1,2,3}, {10,11,12,13}, {20,21,22,23} }; What does a[1,2] evaluate to, and what does the correct a[1][2] give? (Assume you print a[1][2].)
Forecast: these two look identical but return different types . Guess what a[1,2] even is .
Decode a[1,2]. Inside [ ], 1,2 is the comma operator : it evaluates 1, discards it, and yields 2. So a[1,2] means a[2].
Why this step? C has no multi-index subscript. [expr] takes a single expression; the comma operator is a single expression that just happens to hold two.
What is a[2]? A whole row — type int[4], decaying to int* pointing at &a[2][0] (the value 20's address). It is not a single int.
Why this step? One subscript on a 2D array peels one dimension, leaving a row pointer.
Correct a[1][2]. Row 1 is {10,11,12,13}; column 2 is 12.
Why this step? Two proper subscripts reach an actual int.
Verify: by the value formula, a[1][2] = 10*(row index within data)… directly: row 1 = {10,11,12,13}, index 2 → 12. ✅ And a[1,2] = a[2] = pointer to the row starting at 20, not the number 12. ✅
Worked example Ex 9 — Cell J: limiting index — reading one-past-the-row
int a[3][4]; Suppose someone writes a[0][4] (column index 4, but valid columns are only 0–3). Which legal element does that flat address coincide with, and why is relying on this dangerous?
Forecast: the flat block doesn't know about "rows". Where does offset "row 0, column 4" actually land?
Compute the offset anyway. i C + j = 0 ⋅ 4 + 4 = 4 elements.
Why this step? The address formula is pure arithmetic — it doesn't check bounds. Offset 4 is a real spot in the flat block.
Find which valid element sits at offset 4. a [ 1 ] [ 0 ] has offset 1 ⋅ 4 + 0 = 4 .
Why this step? Offset 4 is exactly the start of row 1. So a[0][4] aliases a[1][0] — one past the end of row 0 is the beginning of row 1.
Why this is a trap. It's only "safe" because the row happens to be followed by more of the same array. a[0][4] is still technically undefined behaviour per the C standard (indexing past a row's bound), even though in practice it hits a[1][0]. Never rely on it.
Why this step? Contrast with a[2][4]: offset 2 ⋅ 4 + 4 = 12 , but the array only has 3 ⋅ 4 = 12 elements (indices 0–11), so offset 12 is genuinely outside the whole block — real corruption.
Verify: &a[0][4] offset = 4 ⋅ s ; &a[1][0] offset = ( 1 ⋅ 4 + 0 ) ⋅ s = 4 ⋅ s . Equal. ✅ &a[2][4] offset = 12 ⋅ s = total size, i.e. one-past-the-whole-array. ✅ Both computed, one aliases inside, one escapes.
Recall Quick self-test (reveal each)
Offset (in elements) of a[2][3] in int a[4][5]? ::: 2 ⋅ 5 + 3 = 13 .
&a[0][0] compared to (int*)a? ::: Identical — first element sits at the base.
Offset of last element a[3][4] in int a[4][5]? ::: 19 elements = 76 bytes past base.
a+1 vs *a+1 for int a[3][4], base 1000 — the two addresses? ::: 1016 (a full row) and 1004 (one int).
sizeof(a), sizeof(a[0]), sizeof(a[0][0]) for int a[3][4]? ::: 48 , 16 , 4 bytes.
Offset of a[1][2][3] in int a[2][3][4]? ::: 1 ⋅ 12 + 2 ⋅ 4 + 3 = 23 elements.
What does a[1,2] mean? ::: The comma operator gives a[2] — a whole row pointer, not a single int.
Which valid element does a[0][4] alias in int a[3][4]? ::: a[1][0] (both at offset 4).
Multi-dimensional arrays — the parent: storage, formula, pointer identity.
C Arrays (1D) — the degenerate single-row/column cases collapse to this.
Pointers in C — Ex 5's a+i vs *a+i is pure pointer arithmetic.
Array Decay — why a becomes int(*)[4] (Ex 5, Ex 6).
Pointer to Array vs Array of Pointers — the int(*)[4] type in Ex 5.
sizeof Operator — Ex 6 in full.
Row-major vs Column-major — why offsets use i C + j (Ex 1–4, 9).
Variable Length Arrays (C99) — passing these grids to functions.