Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Example of Row Major Order

Now that you know how row-major order serialises a multi-dimensional array into a flat memory ribbon, let’s cement the idea with a concrete worked example. We’ll use a three-dimensional array — one dimension more than you’re used to — so the pattern really sinks in.


The Array: 2 × 2 × 3

Consider a 3D integer array with these dimensions:

DimensionSymbolSize
Outermost (layer)D₃2
Middle (row)D₂2
Innermost (column)D₁3

Think of it as 2 layers, each layer being a 2×3 grid. The logical representation looks like this:

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
flowchart TB
  subgraph L0["Layer 0  ── D₃ = 0"]
    direction LR
    subgraph R00["D₂ = 0"]
      direction LR
      a000["[0][0][0]"] --- a001["[0][0][1]"] --- a002["[0][0][2]"]
    end
    subgraph R01["D₂ = 1"]
      direction LR
      a010["[0][1][0]"] --- a011["[0][1][1]"] --- a012["[0][1][2]"]
    end
    R00 ~~~ R01
  end
  subgraph L1["Layer 1  ── D₃ = 1"]
    direction LR
    subgraph R10["D₂ = 0"]
      direction LR
      a100["[1][0][0]"] --- a101["[1][0][1]"] --- a102["[1][0][2]"]
    end
    subgraph R11["D₂ = 1"]
      direction LR
      a110["[1][1][0]"] --- a111["[1][1][1]"] --- a112["[1][1][2]"]
    end
    R10 ~~~ R11
  end
  L0 ~~~ L1

Logical representation of the 3D array (D₃=2, D₂=2, D₁=3) — 2 layers, each a 2×3 grid, totalling 12 elements.

Each element is identified by three indices: [layer][row][column], or [I₃][I₂][I₁].


Layout in Memory

Now we flatten this 3D structure into a single linear strip. Row-major order applies the same rule as always:

The lowest dimension (D₁ — column) moves the fastest. The highest dimension (D₃ — layer) moves the slowest.

Think of the three indices as an odometer with three digits. The rightmost digit (D₁ — column) ticks up on every step. The middle digit (D₂ — row) only increments when the column overflows. The leftmost digit (D₃ — layer) only increments when both column and row overflow.

The complete traversal order looks like this:

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
flowchart LR
  subgraph L0["Layer 0  (D₃=0)"]
    direction LR
    subgraph L0R0["D₂=0  —  D₁ races 0→1→2"]
      direction LR
      a000["[0][0][0]"] --> a001["[0][0][1]"] --> a002["[0][0][2]"]
    end
    subgraph L0R1["D₂=1  —  D₁ races 0→1→2"]
      direction LR
      a010["[0][1][0]"] --> a011["[0][1][1]"] --> a012["[0][1][2]"]
    end
    a002 -->|"D₂ increments"| a010
  end
  subgraph L1["Layer 1  (D₃=1)"]
    direction LR
    subgraph L1R0["D₂=0  —  D₁ races 0→1→2"]
      direction LR
      a100["[1][0][0]"] --> a101["[1][0][1]"] --> a102["[1][0][2]"]
    end
    subgraph L1R1["D₂=1  —  D₁ races 0→1→2"]
      direction LR
      a110["[1][1][0]"] --> a111["[1][1][1]"] --> a112["[1][1][2]"]
    end
    a102 -->|"D₂ increments"| a110
  end
  a012 -->|"D₃ increments"| a100

The lowest dimension D₁ moves the fastest in row-major order — it completes a full sweep before D₂ ticks up, and D₂ completes a full sweep before D₃ ticks up.

The resulting serialisation order is:

[0][0][0] → [0][0][1] → [0][0][2] → [0][1][0] → [0][1][1] → [0][1][2]
→ [1][0][0] → [1][0][1] → [1][0][2] → [1][1][0] → [1][1][1] → [1][1][2]

Twelve elements, one after another, lowest dimension cycling fastest.


Structure in Memory

Let’s now map this onto actual memory. Assume:

  • Base address = 2
  • Element size = 4 bytes (integer)

Each element occupies 4 bytes. The address of the element at offset k is:

address = 2 + k × 4

Here is exactly what the array looks like in memory, laid out slot by slot:

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
block-beta
  columns 6
  A["[0][0][0]<br/>addr 2"]
  B["[0][0][1]<br/>addr 6"]
  C["[0][0][2]<br/>addr 10"]
  D["[0][1][0]<br/>addr 14"]
  E["[0][1][1]<br/>addr 18"]
  F["[0][1][2]<br/>addr 22"]
  G["[1][0][0]<br/>addr 26"]
  H["[1][0][1]<br/>addr 30"]
  I["[1][0][2]<br/>addr 34"]
  J["[1][1][0]<br/>addr 38"]
  K["[1][1][1]<br/>addr 42"]
  L["[1][1][2]<br/>addr 46"]

Structure of the 3D array stored in row-major order in memory — 12 elements, base address 2, each element 4 bytes wide.

Notice the pattern: Layer 0 occupies addresses 2–22, Layer 1 occupies 26–46. Within each layer, Row 0 comes first, Row 1 second. Within each row, the column index climbs 0→1→2.

# Verify the memory layout formula
base = 2
element_size = 4

D3, D2, D1 = 2, 2, 3

print(f"{'Index':<16} {'Offset':>6} {'Address':>8}")
print("-" * 32)
for i3 in range(D3):
    for i2 in range(D2):
        for i1 in range(D1):
            offset  = i3 * (D2 * D1) + i2 * D1 + i1
            address = base + offset * element_size
            print(f"[{i3}][{i2}][{i1}]          {offset:>6}    {address:>6}")

Calculating the Address of Elements

When you write array[0][0][2] or array[1][1][2] in code, the subscript operator silently computes the memory address using the formula we derived in the previous lesson — now extended to three dimensions:

offset  = I₃ × (D₂ × D₁)  +  I₂ × D₁  +  I₁
address = base + offset × element_size

Each term in the offset formula represents one “level” of skipping:

  • I₃ × (D₂ × D₁) — skip past I₃ complete layers, each of size D₂ × D₁
  • I₂ × D₁ — skip past I₂ complete rows within the current layer, each of size D₁
  • I₁ — step I₁ positions into the current row

Let’s work through both examples:

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
flowchart LR
  subgraph Calc1["array[0][0][2]  —  I₃=0, I₂=0, I₁=2"]
    direction TB
    C1A["Layer skip<br/>I₃ × (D₂×D₁) = 0 × (2×3) = 0"]
    C1B["Row skip<br/>I₂ × D₁ = 0 × 3 = 0"]
    C1C["Column step<br/>I₁ = 2"]
    C1D["offset = 0 + 0 + 2 = 2"]
    C1E["address = 2 + (2 × 4) = 10 ✓"]
    C1A --> C1B --> C1C --> C1D --> C1E
  end
  subgraph Calc2["array[1][1][2]  —  I₃=1, I₂=1, I₁=2"]
    direction TB
    C2A["Layer skip<br/>I₃ × (D₂×D₁) = 1 × (2×3) = 6"]
    C2B["Row skip<br/>I₂ × D₁ = 1 × 3 = 3"]
    C2C["Column step<br/>I₁ = 2"]
    C2D["offset = 6 + 3 + 2 = 11"]
    C2E["address = 2 + (11 × 4) = 46 ✓"]
    C2A --> C2B --> C2C --> C2D --> C2E
  end

Calculating the base address for array[0][0][2] (offset 2, address 10) and array[1][1][2] (offset 11, address 46) using the subscript operator formula.

Cross-check against the memory layout diagram above: array[0][0][2] is at address 10 ✓ and array[1][1][2] is the very last element at address 46 ✓.

# Subscript operator formula — verify both elements
base = 2
element_size = 4
D2, D1 = 2, 3   # layer size is D2 × D1

def address_of(i3, i2, i1):
    offset = i3 * (D2 * D1) + i2 * D1 + i1
    return base + offset * element_size, offset

addr, offset = address_of(0, 0, 2)
print(f"array[0][0][2] → offset={offset}, address={addr}")   # expect 2, 10

addr, offset = address_of(1, 1, 2)
print(f"array[1][1][2] → offset={offset}, address={addr}")   # expect 11, 46

Dereferencing the Value

Once the subscript operator resolves the address, the program still needs to read the actual value stored there. This step is called dereferencing.

The language already knows two things:

  1. The starting address — computed by the formula above
  2. The datatype size — for int, that’s 4 bytes

It simply reads element_size consecutive bytes starting at that address and interprets the bit pattern as the stored datatype:

---
config:
  theme: base
  themeVariables:
    primaryColor: "#dbeafe"
    primaryBorderColor: "#3b82f6"
    primaryTextColor: "#1e3a5f"
    lineColor: "#64748b"
    secondaryColor: "#ede9fe"
    tertiaryColor: "#fef9c3"
---
flowchart LR
  Addr["Resolved address<br/>e.g. 10"]
  Read["Read 4 consecutive bytes<br/>from address 10, 11, 12, 13"]
  Interp["Interpret as the stored datatype<br/>(int → 4 bytes → one integer value)"]
  Val["Return value to caller<br/>array[0][0][2] = value ✓"]

  Addr --> Read --> Interp --> Val

Dereferencing — once the address is known, the language reads element_size bytes starting there and interprets them according to the stored datatype.

The same mechanism works for any datatype — float, double, char, or even a struct. The only thing that changes is how many bytes are read and how those bytes are interpreted. The array indexing formula itself stays identical.


Key Takeaways

  • A 3D array D₃ × D₂ × D₁ is flattened layer by layer, row by row, column by column
  • D₁ (innermost) moves fastest; D₃ (outermost) moves slowest — same odometer rule for any N dimensions
  • The 3D address formula: base + (I₃ × D₂ × D₁ + I₂ × D₁ + I₁) × element_size
  • After the address is computed, the language reads element_size bytes from that address and interprets them as the stored type — this is dereferencing
  • All of this happens invisibly every time you write arr[i][j][k] — the subscript operator does the maths for you