5.1.11 · D4C Programming

Exercises — Multi-dimensional arrays

2,467 words11 min readBack to topic

Before we start, one picture of the flat memory so every "skip 4 boxes" below has something to point at.

Figure — Multi-dimensional arrays

The grey number under each box is its linear position (how many elements from the base). That linear position is exactly the value of . Keep this strip in mind — every address problem is just "which box number?".


L1 — Recognition

Exercise 1.1 (L1)

For the declaration int a[3][4];, answer without arithmetic: (a) How many ints in total? (b) What is the type of a[i]? (c) What is the type of a[i][j]?

Recall Solution 1.1

(a) ints. (b) a[i] is "array of 4 ints" — written int[4]. Why? a is an array of 3 things, and each thing is a row of 4 ints. (c) a[i][j] is a single int.

This is the "array of arrays" idea from C Arrays (1D): peel one subscript, drop one dimension.

Exercise 1.2 (L1)

True or false: to compute the address of a[i][j] the compiler needs the row count .

Recall Solution 1.2

False. Look at the formula: . Only appears; never does. That is why you may omit the first dimension but not the others.

Exercise 1.3 (L1)

Rewrite a[2][1] using only the * (dereference) operator and +.

Recall Solution 1.3

From Pointers in C: a[i] means *(a+i), applied twice.


L2 — Application

Exercise 2.1 (L2)

int a[3][4]; with base = 2000 and sizeof(int) = 4. Find &a[1][3] (the byte address).

Recall Solution 2.1
  • .
  • elements skipped .
  • bytes .
  • address .

On the strip figure, box 7 sits at the start of the second row's last cell — exactly where a[1][3] lives.

Exercise 2.2 (L2)

char c[5][6]; with base = 100, sizeof(char) = 1. Find &c[3][2].

Recall Solution 2.2
  • .
  • skipped .
  • bytes .
  • address .

Note here, not the number of rows (5). We used the column count, always.

Exercise 2.3 (L2)

Given int m[2][3] = {10, 20, 30, 40, 50, 60};, what is m[1][2]?

Recall Solution 2.3

A flat initializer fills in row-major order:

  • Row 0 gets {10, 20, 30}
  • Row 1 gets {40, 50, 60} So m[1][2] = third element of row 1 = 60.

L3 — Analysis

Exercise 3.1 (L3)

int a[3][4]; with base = 1000, sizeof(int) = 4. Compute a + 1, a[0] + 1, and &a[0][0] + 1 as byte addresses. Explain why two of them differ.

Recall Solution 3.1
  • a decays to int (*)[4] — a pointer to a row of 4 ints. Adding 1 jumps one whole row bytes. a + 1 = .
  • a[0] is int[4], which decays to int*. Adding 1 jumps one int bytes. a[0] + 1 = .
  • &a[0][0] is int*. Adding 1 jumps one int bytes. &a[0][0] + 1 = .

Why the difference: all three point at the same byte (1000), but their types differ, so "+1" means different distances. This is the heart of Array Decay and Pointer to Array vs Array of Pointers — pointer arithmetic scales by the pointed-to type's size.

Exercise 3.2 (L3)

int a[3][5]; with base = 4000, sizeof(int) = 4. Find the linear index and byte address of a[2][4] (the last element). Then confirm it is the last by counting total elements.

Recall Solution 3.2
  • , so linear index .
  • byte address .
  • Total elements , indices . Index 14 is indeed the last, so a[2][4] is the final element. ✔

Exercise 3.3 (L3)

sizeof reasoning. For int a[3][4] with sizeof(int)=4, give (a) sizeof(a), (b) sizeof(a[0]), (c) sizeof(a[0][0]).

Recall Solution 3.3
  • (a) sizeof(a) = whole array = bytes.
  • (b) sizeof(a[0]) = one row of 4 ints = bytes.
  • (c) sizeof(a[0][0]) = one int = bytes.

See sizeof Operator: sizeof reports the type's size, and the type shrinks by one dimension each subscript.


L4 — Synthesis

Exercise 4.1 (L4)

Generalize to 3D. int a[2][3][4] with base = 0, sizeof(int) = 4. Find the byte address of a[1][2][3].

Recall Solution 4.1

Dimensions . Each index is multiplied by the product of all dimensions to its right:

  • multiplies .
  • multiplies .
  • multiplies .
  • linear index .
  • byte address .

Sanity check: total elements , indices ; a[1][2][3] is the last element → index 23. ✔

Exercise 4.2 (L4)

Write a correct function int sum2d(int rows, int cols, int a[][/*?*/]) that sums any 2D int array — using a VLA parameter (C99) so it works for any column count. Explain why a plain int a[][] would not compile.

Recall Solution 4.1
// VLA parameter: cols is a runtime value that fixes the inner dimension.
int sum2d(int rows, int cols, int a[rows][cols]) {
    int total = 0;
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            total += a[i][j];
    return total;
}

Why int a[][] fails: inside the loop the compiler computes a[i][j] via i*cols + j. Without cols it cannot do the i*cols step. So the inner dimension must be known — here we make it a runtime parameter via a VLA. See also Array Decay: a arrives as int (*)[cols].

Exercise 4.3 (L4)

A 2D array int a[R][C] is stored column-major instead (Fortran/MATLAB style). Derive the address formula and compute the linear index of a[2][1] for R=3, C=4.

Recall Solution 4.3

Column-major stores all of column 0, then all of column 1, etc. To reach column we skip full columns, each with elements . Then move down inside the column. For a[2][1], : linear index . Compare row-major for the same element: . Different box! That is exactly the Row-major vs Column-major distinction — same logical element, different physical slot.

Figure — Multi-dimensional arrays

L5 — Mastery

Exercise 5.1 (L5)

Inverse problem. int a[3][4] (row-major, sizeof(int)=4) with base = 5000. You are told an element sits at byte address 5040. Recover its [i][j] indices.

Recall Solution 5.1
  • Offset in bytes .
  • Linear index .
  • Now invert with :
    • (integer division — how many full rows fit).
    • (the remainder — position inside the row).
  • Element is .

Why divide and mod? Row-major packs elements per row, so dividing the linear index by counts complete rows (that's ), and the remainder is how far into the current row you are (that's ). This is the exact inverse of .

Exercise 5.2 (L5)

Design + verify. You want to store a RGB-less brightness cube int b[2][2][2] and fill it so that every element equals its own linear index ( through ). Write the fill loop, then state the value of b[1][0][1] and prove it equals the address-formula prediction.

Recall Solution 5.2
int b[2][2][2];
for (int i = 0; i < 2; i++)
  for (int j = 0; j < 2; j++)
    for (int k = 0; k < 2; k++)
      b[i][j][k] = i*(2*2) + j*2 + k;   // = linear index, YZ=4, Z=2

For b[1][0][1]: linear index , so b[1][0][1] == 5. Proof it matches storage: with base=0, sizeof(int)=4, the byte address is , i.e. slot 5 on the flat strip — exactly the value we stored. The fill formula is the address formula (without the ), so value and position agree by construction.

Exercise 5.3 (L5)

Full synthesis. Prove the parent note's claim: for T a[R][C], a[i][j] and *(*(a+i)+j) reference the same element. Do it by tracking types and byte offsets, with , sizeof(int)=4, base=B.

Recall Solution 5.3

Follow *(*(a+i)+j) from the inside out:

  1. a decays to int(*)[4] (pointer to a 4-int row), pointing at byte .
  2. a + i: adds rows. Each row is bytes, so this points at byte .
  3. *(a+i): dereference gives the row int[4], which decays to int* still at byte .
  4. + j: adds ints bytes → byte .
  5. outer *: read the int at byte .

Now the subscript form via the address formula: Same byte, same type (int). Therefore a[i][j]*(*(a+i)+j). The two-step pointer walk is the row-major arithmetic in disguise. ∎


Recall One-line recap of every formula used

Row-major 2D ::: Column-major 2D ::: 3D row-major ::: Invert linear index ::: ,

Connections