30 min read

> "The array is the simplest and most widely used data structure. It is so fundamental that nearly every programming language provides it as a built-in feature." — Niklaus Wirth, Algorithms + Data Structures = Programs (1976)

Chapter 9: Arrays: Fixed Collections and Multidimensional Data

"The array is the simplest and most widely used data structure. It is so fundamental that nearly every programming language provides it as a built-in feature." — Niklaus Wirth, Algorithms + Data Structures = Programs (1976)

Up to now, every variable we have declared holds exactly one value. A single integer. A single real number. A single character. That works beautifully when the problem is small — compute the area of one circle, convert one temperature, process one student's grade. But real problems rarely involve just one of anything.

Consider a teacher who needs to store grades for thirty students. With the tools we have so far, we would need thirty separate variables: grade1, grade2, grade3, ... all the way to grade30. That is tedious to declare, impossible to loop over cleanly, and utterly impractical if the number of students changes. What we need is a way to store a collection of values under a single name, where each value can be accessed by its position.

That is precisely what an array provides. In this chapter, we introduce arrays — Pascal's primary mechanism for storing fixed-size, ordered collections of data. We will learn how to declare arrays with custom index ranges, iterate over them, pass them to subprograms, work with two-dimensional grids, and even use dynamic arrays whose size can change at runtime. Along the way, we will build several practical algorithms — searching, accumulating, finding extremes — that form the backbone of data processing in any language.

By the end of this chapter, we will have introduced GradeBook Pro, the anchor example that will grow with us through Part II, and we will extend our PennyWise project with array-based expense tracking and monthly totals.


9.1 What Is an Array?

An array is a data structure that stores a fixed number of elements of the same type in contiguous memory locations. Each element is identified by an index (also called a subscript), and we can access any element in constant time — meaning it takes the same amount of effort to reach the first element as it does to reach the hundredth.

Three properties define an array:

  1. Homogeneous: Every element has the same data type. An array of integers holds only integers; an array of characters holds only characters.
  2. Contiguous: Elements are stored next to each other in memory. If the first element lives at memory address 1000 and each element occupies 4 bytes, then the second element lives at 1004, the third at 1008, and so on.
  3. Indexed: Each element has a unique index that identifies its position. Pascal allows these indices to be any ordinal type — integers, characters, enumerated types, or subranges.

Why Contiguous Memory Matters

Because array elements sit side by side in memory, the computer can calculate the exact location of any element using simple arithmetic:

address(element[i]) = base_address + (i - low_index) * element_size

This formula means accessing grades[17] is just as fast as accessing grades[1]. No searching required. No chain of pointers to follow. Just one multiplication and one addition. This O(1) random access is the array's defining advantage.

A Mental Model

Think of an array as a row of numbered mailboxes in an apartment building. Every mailbox is the same size (homogeneous). They are mounted side by side on the wall (contiguous). Each mailbox has a number on it (indexed). If you know the mailbox number, you can walk directly to it without checking any other mailbox.

+-------+-------+-------+-------+-------+-------+
|  82   |  91   |  76   |  88   |  95   |  64   |
+-------+-------+-------+-------+-------+-------+
  [1]     [2]     [3]     [4]     [5]     [6]

Here we have an array of six integer grades. The value at index 1 is 82, the value at index 3 is 76, and so on.

When Do We Need Arrays?

You need an array whenever your program must:

  • Store multiple values and process them later. If you read 100 temperatures and then need to find the average, you cannot compute the average until you have seen all the values. You need somewhere to store them while reading.
  • Access values by position. If you need "the third student's grade" or "the temperature on day 200," an array gives you direct access.
  • Process a collection repeatedly. You might scan grades to find the average, then scan again to find how many are above average. Without an array, you would have to re-read the input each time.
  • Relate multiple pieces of data by shared position. If names[5] is "Alice" and grades[5] is 92, the index 5 connects them. This "parallel arrays" pattern is our first step toward structured data.

Contrast this with what we could do before: we could read values one at a time in a loop and accumulate a running sum, but we could never go back and look at an earlier value. Arrays give us random access to the entire collection.


9.2 Declaring Arrays in Pascal

Pascal's array syntax is one of its great design strengths. Unlike many languages that force zero-based indexing, Pascal lets us choose any ordinal range as our index type. This makes code more readable and less error-prone.

Basic Syntax

var
  grades: array[1..30] of Integer;

This declares an array named grades that holds 30 integers, indexed from 1 to 30. The general form is:

array[low..high] of ElementType;

where low and high are constants of any ordinal type, and ElementType is the type of each element.

Custom Index Ranges

Pascal's flexibility with index ranges is remarkably powerful. We are not limited to starting at 0 or 1:

var
  { Index from 1 to 12 — natural for months }
  monthlyTotals: array[1..12] of Real;

  { Index from 0 to 99 — zero-based when appropriate }
  buffer: array[0..99] of Byte;

  { Negative indices — useful for coordinate offsets }
  offsets: array[-5..5] of Integer;

  { Year range — self-documenting code }
  population: array[1900..2030] of LongInt;

That last example is particularly elegant. Instead of storing population data in pop[0] through pop[130] and mentally adding 1900 every time we access it, we can write population[1965] and mean exactly what we say. The code documents itself.

Character Indices

Since characters are ordinal types in Pascal, we can use them as array indices:

var
  letterCount: array['A'..'Z'] of Integer;

This creates 26 integer slots, one for each uppercase letter. To count occurrences of the letter 'E', we simply write letterCount['E'] := letterCount['E'] + 1. No arithmetic to convert letters to positions — Pascal handles it.

Enumerated Type Indices

When combined with the enumerated types we met in Chapter 8, arrays become even more expressive:

type
  TDay = (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
var
  hoursWorked: array[TDay] of Real;

Now we can write hoursWorked[Fri] := 8.0 instead of remembering that Friday is index 4 (or 5, depending on whether we started at 0 or 1). This is Theme 1 — Teaches Right in action: Pascal encourages us to write code that matches our mental model of the problem.

Named Array Types

For any array we plan to use more than once — especially if we will pass it to subprograms — we should define a named type:

type
  TGradeArray = array[1..30] of Integer;
var
  mathGrades:    TGradeArray;
  scienceGrades: TGradeArray;

This is not merely a convenience. As we will see in Section 9.4, Pascal requires that array parameters match by type name, not just by structure. Two arrays with identical structure but different type names are considered incompatible. Defining named types is therefore essential practice.

Constant Arrays

Pascal allows typed constants — arrays initialized at declaration time:

const
  DaysInMonth: array[1..12] of Integer =
    (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);

These values are set at compile time and (in standard Pascal) cannot be changed. They are ideal for lookup tables, conversion factors, and other fixed data.

Initialization

When you declare an array variable (not a typed constant), its contents are undefined — they contain whatever bits happened to be in that memory location. Always initialize before reading:

var
  counts: array[1..10] of Integer;
  i: Integer;
begin
  { Initialize all elements to zero }
  for i := 1 to 10 do
    counts[i] := 0;

Free Pascal also provides the FillChar procedure for bulk initialization, and the Default intrinsic in newer modes, but the explicit loop is clearest and most portable.

Common Declaration Mistakes

Let us catalog the mistakes beginners make with array declarations, so we can avoid them:

Mistake 1: Using a variable as a bound.

var
  n: Integer;
  data: array[1..n] of Integer;  { COMPILE ERROR — n is not a constant }

Static array bounds must be compile-time constants. If you need the size to depend on a variable, use a dynamic array (Section 9.7) or declare the array with a generous maximum size and track the actual count separately.

Mistake 2: Forgetting that types must match by name.

var
  a: array[1..10] of Integer;
  b: array[1..10] of Integer;
begin
  a := b;  { COMPILE ERROR in strict mode — different anonymous types }

Even though a and b have identical structures, the compiler treats each array[1..10] of Integer declaration as a distinct anonymous type. The solution is to define a named type and use it for both variables.

Mistake 3: Off-by-one with ranges.

var
  data: array[0..9] of Integer;  { 10 elements: indices 0..9 }
begin
  for i := 1 to 10 do  { BUG — should be 0 to 9 }
    data[i] := 0;       { data[10] is out of bounds! }

This is why Low and High are so valuable — they eliminate this entire class of error.


9.3 Working with Arrays

Accessing Elements

We access individual array elements using the index in square brackets:

grades[1] := 82;        { assign a value }
WriteLn(grades[1]);      { read a value: prints 82 }
grades[3] := grades[1];  { copy one element to another }

The index can be any expression that evaluates to a value within the declared range:

var
  i: Integer;
begin
  i := 5;
  grades[i] := 90;          { index is a variable }
  grades[i + 1] := 88;      { index is an expression }
  grades[2 * i - 3] := 77;  { index is a more complex expression }

Iterating Over Arrays

The most common operation is visiting every element in order. A for loop is the natural choice:

{ Print all 30 grades }
for i := 1 to 30 do
  WriteLn('Student ', i, ': ', grades[i]);

For arrays indexed by non-integer ordinal types, we use the same for loop:

for day := Mon to Sun do
  WriteLn(day, ': ', hoursWorked[day]:0:1, ' hours');

Bounds Checking

What happens if we access an index outside the declared range? In Pascal with range checking enabled (the default in Free Pascal's debug mode), we get a runtime error:

var
  a: array[1..10] of Integer;
begin
  a[11] := 42;  { Runtime Error 201: Range check error }

This is a safety feature. In languages like C, an out-of-bounds access silently corrupts memory — a notorious source of security vulnerabilities. Pascal catches the mistake immediately.

Tip

Always compile with range checking enabled during development ({$R+} in Free Pascal). Turn it off only for performance-critical release builds after thorough testing.

Whole-Array Assignment

If two arrays have the same named type, Pascal allows direct assignment:

type
  TScores = array[1..10] of Integer;
var
  original, backup: TScores;
begin
  { ... fill original ... }
  backup := original;  { copies all 10 elements }

This copies every element from original to backup. It is concise and efficient. Note, however, that the types must be identically named — two arrays declared with the same structure but different type names cannot be assigned to each other. This strictness is deliberate: it prevents accidental mixing of conceptually different data.

Array Comparison

Pascal does not support direct comparison of arrays with = or <>. You cannot write if a = b then ... for two arrays. Instead, you must compare element by element:

function ArraysEqual(const a, b: TScores): Boolean;
var
  i: Integer;
begin
  ArraysEqual := True;
  for i := Low(a) to High(a) do
    if a[i] <> b[i] then
    begin
      ArraysEqual := False;
      Exit;
    end;
end;

This is a good candidate for a utility function that you write once and reuse across your programs. Notice how the function exits early if it finds any mismatch — there is no need to compare the remaining elements once we know the arrays differ.

Partially Filled Arrays

In practice, we often declare an array larger than we need and use a separate variable to track how many elements are actually in use. This is the partially filled array pattern:

const
  MAX_SIZE = 100;
type
  TDataArray = array[1..MAX_SIZE] of Integer;
var
  data: TDataArray;
  count: Integer;  { number of elements actually stored }

Here, data[1] through data[count] hold valid values, and data[count+1] through data[MAX_SIZE] are unused (and contain garbage). Every operation on this array must use count as the upper bound — not MAX_SIZE. We will see this pattern throughout GradeBook Pro and PennyWise.

Warning

A partially filled array wastes memory when count is much smaller than MAX_SIZE. It also imposes an absolute ceiling: if you need more than MAX_SIZE elements, you are stuck. Dynamic arrays (Section 9.7) address both problems, at the cost of slightly more complex code.

The Low, High, and Length Functions

Free Pascal provides built-in functions that make array code more robust:

var
  data: array[5..20] of Real;
  i: Integer;
begin
  for i := Low(data) to High(data) do
    data[i] := 0.0;

  WriteLn('Array has ', Length(data), ' elements');
  { Prints: Array has 16 elements }
  • Low(data) returns the lowest valid index (5)
  • High(data) returns the highest valid index (20)
  • Length(data) returns the number of elements (16)

Using these functions instead of hard-coded bounds means our loops automatically adapt if we change the array declaration. This is a hallmark of maintainable code.


9.4 Passing Arrays to Subprograms

Arrays become truly powerful when combined with the procedures and functions we learned in Chapter 7. We can write a single FindMaximum function and use it with any array of the right type — instead of duplicating the logic every time.

Passing by Reference

Because arrays can be large, passing them by value (which creates a full copy) can be expensive. When we need to modify the array, we pass by reference:

type
  TIntArray = array[1..100] of Integer;

procedure FillWithZeros(var arr: TIntArray);
var
  i: Integer;
begin
  for i := Low(arr) to High(arr) do
    arr[i] := 0;
end;

The var keyword means the procedure works directly on the caller's array — no copy is made, and changes persist after the procedure returns.

Passing by Const Reference

When a subprogram needs to read an array but not modify it, use const:

function Sum(const arr: TIntArray): Integer;
var
  i, total: Integer;
begin
  total := 0;
  for i := Low(arr) to High(arr) do
    total := total + arr[i];
  Sum := total;
end;

The const keyword tells Pascal (and the reader) that this function will not alter the array. The compiler enforces this — any attempt to assign to arr[i] inside the function is a compile-time error. As a bonus, the compiler may optimize const parameters by passing the address rather than copying, giving us the efficiency of var with the safety of value passing.

Open Array Parameters

What if we want a function that works on arrays of any size — not just arrays of exactly 100 elements? Pascal provides open array parameters:

function Average(const arr: array of Integer): Real;
var
  i, total: Integer;
begin
  total := 0;
  for i := 0 to High(arr) do
    total := total + arr[i];
  Average := total / Length(arr);
end;

An open array parameter is declared as array of ElementType without specifying bounds. Inside the function:

  • The array is always zero-indexed, regardless of the caller's index range. Low returns 0; High returns Length - 1.
  • Length(arr) returns the actual number of elements passed.

We can call this function with arrays of any size:

var
  small: array[1..5] of Integer;
  large: array[1..1000] of Integer;
begin
  WriteLn(Average(small):0:2);   { works }
  WriteLn(Average(large):0:2);   { also works }

Open array parameters are one of Pascal's most practical features for writing reusable code. They embody Theme 5 — A + DS = P (Algorithms + Data Structures = Programs): the algorithm (averaging) is separated from the data structure's specific size.

Important subtlety: Inside a function with an open array parameter, the array is always zero-indexed — even if the caller's array is indexed 1..5 or 'A'..'Z'. The mapping happens automatically: the caller's first element becomes index 0, the second becomes index 1, and so on. Always use 0 to High(arr) for your loops, never 1 to Length(arr) (which would skip the first element and access one past the end).

Putting It Together: A Reusable Statistics Module

Let us combine what we know about parameter passing into a set of reusable functions that work with any integer array:

{ These functions accept any integer array thanks to open array parameters }

function ArraySum(const arr: array of Integer): LongInt;
var
  i: Integer;
  total: LongInt;
begin
  total := 0;
  for i := 0 to High(arr) do
    total := total + arr[i];
  ArraySum := total;
end;

function ArrayAverage(const arr: array of Integer): Real;
begin
  if Length(arr) = 0 then
    ArrayAverage := 0.0
  else
    ArrayAverage := ArraySum(arr) / Length(arr);
end;

procedure ArrayMinMax(const arr: array of Integer;
                       var minVal, maxVal: Integer);
var
  i: Integer;
begin
  minVal := arr[0];
  maxVal := arr[0];
  for i := 1 to High(arr) do
  begin
    if arr[i] < minVal then minVal := arr[i];
    if arr[i] > maxVal then maxVal := arr[i];
  end;
end;

This is a small library of array utilities. We can call ArraySum(grades), ArraySum(temperatures), or ArraySum(scores) — the same function works for all of them. This is the power of separating the algorithm from the specific array type, and it is a foundational principle that we will revisit throughout this textbook.

Type Compatibility Rules

Pascal is strict about type compatibility for array parameters. If a procedure expects TIntArray, you must pass a variable of type TIntArray — not a variable that happens to have the same structure. This is why defining named types is critical:

type
  TGrades = array[1..30] of Integer;
  TScores = array[1..30] of Integer;  { same structure, different type! }

procedure ProcessGrades(var g: TGrades);
begin
  { ... }
end;

var
  myGrades: TGrades;
  myScores: TScores;
begin
  ProcessGrades(myGrades);  { OK — same type }
  ProcessGrades(myScores);  { COMPILE ERROR — different type! }

This strictness prevents us from accidentally passing a score array to a grade-processing function, even though both are arrays of 30 integers. In a large program, this catches real bugs.


9.5 Common Array Algorithms

Arrays are not just containers — they are the foundation for fundamental algorithms that appear in virtually every non-trivial program. Let us build a toolkit of essential array operations.

The simplest search algorithm: examine each element in order until we find what we are looking for (or reach the end).

function LinearSearch(const arr: array of Integer;
                       target: Integer): Integer;
{ Returns the index of target in arr, or -1 if not found }
var
  i: Integer;
begin
  LinearSearch := -1;  { assume not found }
  for i := 0 to High(arr) do
    if arr[i] = target then
    begin
      LinearSearch := i;
      Exit;  { found it — stop searching }
    end;
end;

Linear search examines up to n elements for an array of size n. It is simple and works on unsorted data. For sorted data, binary search (Chapter 19) is dramatically faster — but linear search is where we start.

Accumulation (Sum and Average)

Summing all elements is the most basic accumulation pattern:

function SumArray(const arr: array of Integer): LongInt;
var
  i: Integer;
  total: LongInt;
begin
  total := 0;
  for i := 0 to High(arr) do
    total := total + arr[i];
  SumArray := total;
end;

function AverageArray(const arr: array of Integer): Real;
begin
  if Length(arr) = 0 then
    AverageArray := 0.0
  else
    AverageArray := SumArray(arr) / Length(arr);
end;

Notice the guard against division by zero in AverageArray. Defensive coding like this separates robust programs from fragile ones.

Finding Maximum and Minimum

To find the largest value, we assume the first element is the maximum, then scan through looking for anything larger:

function FindMax(const arr: array of Integer): Integer;
var
  i: Integer;
  maxVal: Integer;
begin
  maxVal := arr[0];  { start with first element }
  for i := 1 to High(arr) do
    if arr[i] > maxVal then
      maxVal := arr[i];
  FindMax := maxVal;
end;

The minimum version simply flips > to <. A variant that returns the index of the maximum (rather than the value itself) is often more useful:

function FindMaxIndex(const arr: array of Integer): Integer;
var
  i, maxIdx: Integer;
begin
  maxIdx := 0;
  for i := 1 to High(arr) do
    if arr[i] > arr[maxIdx] then
      maxIdx := i;
  FindMaxIndex := maxIdx;
end;

Counting

Count how many elements satisfy a condition:

function CountAbove(const arr: array of Integer;
                     threshold: Integer): Integer;
var
  i, count: Integer;
begin
  count := 0;
  for i := 0 to High(arr) do
    if arr[i] > threshold then
      Inc(count);
  CountAbove := count;
end;

Reversing

Reverse an array in place by swapping elements from the ends toward the middle:

procedure ReverseArray(var arr: array of Integer);
var
  lo, hi, temp: Integer;
begin
  lo := 0;
  hi := High(arr);
  while lo < hi do
  begin
    temp := arr[lo];
    arr[lo] := arr[hi];
    arr[hi] := temp;
    Inc(lo);
    Dec(hi);
  end;
end;

This is an elegant O(n/2) algorithm — it touches each element at most once. Notice we only need to go until lo < hi, not all the way through the array.

Shifting and Inserting

To insert an element at position pos, we must first shift everything from pos onward one position to the right to make room:

procedure InsertAt(var arr: array of Integer;
                    var count: Integer;
                    pos: Integer;
                    value: Integer);
var
  i: Integer;
begin
  { Shift elements right from the end }
  for i := count - 1 downto pos do
    arr[i + 1] := arr[i];
  arr[pos] := value;
  Inc(count);
end;

The key insight: we must shift from right to left (using downto), not left to right. If we shifted left to right, we would overwrite elements before copying them. This is a common source of bugs in array manipulation code.

Deletion is the reverse — shift elements left to close the gap:

procedure DeleteAt(var arr: array of Integer;
                    var count: Integer;
                    pos: Integer);
var
  i: Integer;
begin
  for i := pos to count - 2 do
    arr[i] := arr[i + 1];
  Dec(count);
end;

Both insertion and deletion are O(n) operations in the worst case, because they may need to shift nearly every element. This is one of the fundamental trade-offs of arrays: excellent random access, but expensive insertion and deletion in the middle.

Combining Patterns

Real programs combine these primitives. For example, "find the student with the highest grade and print their name" requires FindMaxIndex on the grades array, then using that index to look up the corresponding name. We will see exactly this pattern in GradeBook Pro later in this chapter.

Another common combination is "accumulate then analyze": compute the sum of an array, then scan again to find elements above the average. The first pass computes the average; the second pass uses it as a threshold:

var
  avg: Real;
  i, aboveAvg: Integer;
begin
  avg := AverageArray(grades, count);
  aboveAvg := 0;
  for i := 1 to count do
    if grades[i] > avg then
      Inc(aboveAvg);
  WriteLn(aboveAvg, ' students scored above the class average of ', avg:0:1);
end;

This two-pass pattern is important to understand: you cannot determine how many values are "above average" in a single pass, because you do not know the average until you have processed every element.


9.6 Multidimensional Arrays

Many problems are naturally two-dimensional: a spreadsheet of rows and columns, a chessboard, a pixel grid, a matrix in linear algebra. Pascal handles these with multidimensional arrays.

Declaring 2D Arrays

A two-dimensional array is declared by specifying two index ranges:

var
  grid: array[1..8, 1..8] of Integer;     { 8x8 grid, like a chessboard }
  matrix: array[0..2, 0..3] of Real;      { 3 rows, 4 columns }
  screen: array[0..24, 0..79] of Char;    { 25-line, 80-column text screen }

The first index conventionally represents the row, and the second represents the column. To access the element in row 3, column 5:

grid[3, 5] := 42;

Pascal also accepts the equivalent notation grid[3][5], but the comma form is more idiomatic.

An Equivalent Way to Think About It

A 2D array is technically an array of arrays. This declaration:

type
  TRow = array[1..8] of Integer;
  TGrid = array[1..8] of TRow;

creates the same structure as array[1..8, 1..8] of Integer. Each row is a contiguous block of 8 integers, and there are 8 such rows.

Iterating Over 2D Arrays

We use nested loops — the outer loop for rows, the inner loop for columns:

var
  row, col: Integer;
begin
  { Fill with multiplication table }
  for row := 1 to 10 do
    for col := 1 to 10 do
      table[row, col] := row * col;

  { Print the table }
  for row := 1 to 10 do
  begin
    for col := 1 to 10 do
      Write(table[row, col]:5);
    WriteLn;  { new line after each row }
  end;

Row-Major Layout in Memory

Pascal stores 2D arrays in row-major order: all elements of row 1 come first, then all elements of row 2, and so on. For a 3x4 array:

Memory: [1,1] [1,2] [1,3] [1,4] [2,1] [2,2] [2,3] [2,4] [3,1] [3,2] [3,3] [3,4]
        |--- row 1 ---|   |--- row 2 ---|   |--- row 3 ---|

This matters for performance. When iterating, the inner loop should vary the column index (rightmost), because consecutive memory accesses are faster than scattered ones. This is called spatial locality, and it can make a measurable difference for large arrays. The pattern for row ... for col that we used above is already optimal.

Higher Dimensions

Pascal supports arrays with three or more dimensions:

var
  cube: array[1..10, 1..10, 1..10] of Integer;  { 3D }
  hyperCube: array[1..5, 1..5, 1..5, 1..5] of Integer;  { 4D }

However, arrays beyond two dimensions are relatively rare in practice. Most problems that seem three-dimensional can often be restructured with records or other approaches.

Practical Example: Tic-Tac-Toe Board

A 2D array is the natural representation for a game board:

type
  TCell = (Empty, Cross, Nought);
  TBoard = array[1..3, 1..3] of TCell;

procedure InitBoard(var board: TBoard);
var
  r, c: Integer;
begin
  for r := 1 to 3 do
    for c := 1 to 3 do
      board[r, c] := Empty;
end;

function CheckWin(const board: TBoard; player: TCell): Boolean;
var
  r, c: Integer;
begin
  CheckWin := False;
  { Check rows }
  for r := 1 to 3 do
    if (board[r,1] = player) and (board[r,2] = player) and
       (board[r,3] = player) then
      CheckWin := True;
  { Check columns }
  for c := 1 to 3 do
    if (board[1,c] = player) and (board[2,c] = player) and
       (board[3,c] = player) then
      CheckWin := True;
  { Check diagonals }
  if (board[1,1] = player) and (board[2,2] = player) and
     (board[3,3] = player) then
    CheckWin := True;
  if (board[1,3] = player) and (board[2,2] = player) and
     (board[3,1] = player) then
    CheckWin := True;
end;

This example beautifully illustrates how the right data structure makes the algorithm almost write itself.

Working with Rows and Columns Independently

Sometimes we need to process a single row or a single column of a 2D array. Processing a row is straightforward — it is just a 1D slice:

{ Sum of row r }
function RowSum(const grid: TGrid; r: Integer): Integer;
var
  c, total: Integer;
begin
  total := 0;
  for c := 1 to 8 do
    total := total + grid[r, c];
  RowSum := total;
end;

Processing a column is similar, but we vary the row index instead:

{ Sum of column c }
function ColSum(const grid: TGrid; c: Integer): Integer;
var
  r, total: Integer;
begin
  total := 0;
  for r := 1 to 8 do
    total := total + grid[r, c];
  ColSum := total;
end;

These row and column operations are the building blocks for more complex algorithms like matrix transposition, row reduction in linear algebra, and processing spreadsheet data.

A Practical 2D Example: Seating Chart

Consider a classroom with 5 rows and 6 seats per row. We can represent it as a 2D array where each cell contains the student's ID (or 0 for an empty seat):

type
  TSeatingChart = array[1..5, 1..6] of Integer;

function CountEmptySeats(const chart: TSeatingChart): Integer;
var
  r, c, empty: Integer;
begin
  empty := 0;
  for r := 1 to 5 do
    for c := 1 to 6 do
      if chart[r, c] = 0 then
        Inc(empty);
  CountEmptySeats := empty;
end;

procedure FindStudent(const chart: TSeatingChart;
                       studentID: Integer;
                       var row, col: Integer);
{ Sets row and col to the student's position, or -1 if not found }
var
  r, c: Integer;
begin
  row := -1;
  col := -1;
  for r := 1 to 5 do
    for c := 1 to 6 do
      if chart[r, c] = studentID then
      begin
        row := r;
        col := c;
        Exit;
      end;
end;

This is a 2D linear search — the same algorithm as 1D linear search, but with nested loops to cover both dimensions. The Exit statement breaks out of both loops as soon as we find the student, avoiding unnecessary work.


9.7 Dynamic Arrays

Everything we have covered so far uses static arrays — their size is fixed at compile time. But what if we do not know the size until the program is running? Perhaps we are reading data from a file, or the user will tell us how many items to process.

Free Pascal (and Delphi) support dynamic arrays, which can be resized at runtime.

Declaring Dynamic Arrays

A dynamic array is declared without specifying bounds:

var
  data: array of Integer;      { dynamic array of integers }
  names: array of String;      { dynamic array of strings }
  matrix: array of array of Real;  { dynamic 2D array }

At this point, the array is empty — it has no elements and occupies minimal memory (just a pointer internally).

Setting the Length

We allocate elements using SetLength:

var
  data: array of Integer;
begin
  SetLength(data, 50);  { now has 50 elements: data[0]..data[49] }

Dynamic arrays are always zero-indexed. After SetLength(data, 50), valid indices are 0 through 49.

Resizing

We can resize a dynamic array at any time:

SetLength(data, 100);  { expand to 100 elements; first 50 preserved }
SetLength(data, 20);   { shrink to 20 elements; last 80 lost }

When expanding, existing elements retain their values and new elements are initialized to zero (for numeric types) or their default value. When shrinking, excess elements are discarded.

Dynamic Arrays in Practice

Here is a complete example that reads numbers from the user until they enter -1, storing them in a dynamically growing array:

program DynamicInput;
var
  data: array of Integer;
  count: Integer;
  value: Integer;
begin
  count := 0;
  WriteLn('Enter integers (-1 to stop):');

  repeat
    Write('> ');
    ReadLn(value);
    if value <> -1 then
    begin
      Inc(count);
      SetLength(data, count);
      data[count - 1] := value;  { zero-indexed! }
    end;
  until value = -1;

  WriteLn('You entered ', count, ' values.');
  WriteLn('Sum: ', SumArray(data));
end.

Memory Implications

Each call to SetLength may allocate a new block of memory and copy all existing elements. For an array that grows one element at a time (as above), this means n calls to SetLength for n elements, with copying costs of 1 + 2 + 3 + ... + n = n(n+1)/2 operations total. That is O(n^2) — slow for large datasets.

A better strategy is to allocate in chunks:

const
  CHUNK_SIZE = 64;
var
  data: array of Integer;
  count, capacity: Integer;

procedure AddElement(value: Integer);
begin
  if count >= capacity then
  begin
    capacity := capacity + CHUNK_SIZE;
    SetLength(data, capacity);
  end;
  data[count] := value;
  Inc(count);
end;

This amortized approach reduces the number of reallocations dramatically. We will formalize this pattern when we study lists in a later chapter.

Dynamic 2D Arrays

Dynamic two-dimensional arrays require a slightly different syntax:

var
  matrix: array of array of Real;
  rows, cols, r: Integer;
begin
  rows := 3;
  cols := 4;
  SetLength(matrix, rows, cols);

  { Access: matrix[r][c] — note brackets, not comma }
  for r := 0 to rows - 1 do
    for c := 0 to cols - 1 do
      matrix[r][c] := 0.0;

Note that with dynamic 2D arrays, we use matrix[r][c] (bracket notation) rather than matrix[r, c] (comma notation). Both work, but the bracket form is more common with dynamic arrays.

Static vs. Dynamic: When to Choose

Feature Static Array Dynamic Array
Size Fixed at compile time Set/changed at runtime
Index range Any ordinal range Always 0-based
Memory Stack (usually) Heap
Initialization Undefined Zero/default
Speed Slightly faster Slightly slower (indirection)
Best for Known, fixed collections Unknown or varying size

Use static arrays when the size is known and constant (days in a week, months in a year, a fixed game board). Use dynamic arrays when the size depends on input or runtime conditions.

A Complete Dynamic Array Example: Reading Scores from Input

Let us write a complete program that demonstrates dynamic arrays in a realistic scenario. The program reads test scores from the user (with no predetermined limit), then computes statistics:

program DynamicScores;
{$mode objfpc}{$H+}

var
  scores: array of Integer;
  count: Integer;
  value: Integer;
  i: Integer;
  total: LongInt;
  avg: Real;
  maxScore, minScore: Integer;

begin
  count := 0;
  WriteLn('Enter test scores (-1 to stop):');

  { Read scores into dynamic array }
  repeat
    Write('Score: ');
    ReadLn(value);
    if value <> -1 then
    begin
      Inc(count);
      SetLength(scores, count);
      scores[count - 1] := value;
    end;
  until value = -1;

  if count = 0 then
  begin
    WriteLn('No scores entered.');
    Halt;
  end;

  { Calculate statistics }
  total := 0;
  maxScore := scores[0];
  minScore := scores[0];
  for i := 0 to High(scores) do
  begin
    total := total + scores[i];
    if scores[i] > maxScore then maxScore := scores[i];
    if scores[i] < minScore then minScore := scores[i];
  end;
  avg := total / count;

  { Report }
  WriteLn('--- Results ---');
  WriteLn('Count:   ', count);
  WriteLn('Sum:     ', total);
  WriteLn('Average: ', avg:0:2);
  WriteLn('Highest: ', maxScore);
  WriteLn('Lowest:  ', minScore);
  WriteLn('Range:   ', maxScore - minScore);
end.

This program shows dynamic arrays at their most natural: we genuinely do not know how many scores the user will enter, so we grow the array as needed. In production code we would use the chunked-growth pattern described above, but for small datasets the element-at-a-time approach is perfectly acceptable.

The Copy Function for Dynamic Arrays

Free Pascal provides a Copy function that creates a new dynamic array from a slice of an existing one:

var
  original: array of Integer;
  subset: array of Integer;
begin
  SetLength(original, 5);
  original[0] := 10; original[1] := 20; original[2] := 30;
  original[3] := 40; original[4] := 50;

  subset := Copy(original, 1, 3);  { copies elements [1], [2], [3] }
  { subset now contains [20, 30, 40] with Length = 3 }

The Copy function takes three arguments: the source array, the starting index, and the number of elements. This is useful for extracting portions of arrays, splitting data, or creating working copies without affecting the original.


9.8 Arrays vs. Other Approaches (When Arrays Are Not Enough)

Arrays are remarkably versatile, but they have limitations that point us toward the data structures we will explore in upcoming chapters.

Limitation 1: All Elements Must Be the Same Type

An array of integers can only hold integers. What if we want to store a student's name and their grade and their attendance record? We could use parallel arrays — one array for names, one for grades, one for attendance — but this is fragile. If we sort the grades array, we must remember to rearrange the names and attendance arrays in the same way.

The solution is the record (Chapter 10), which groups related fields of different types into a single structure. We can then create an array of records: the best of both worlds.

Limitation 2: Fixed Size (for Static Arrays)

Even dynamic arrays require us to manage sizes manually. A linked list (which we will cover later) can grow and shrink effortlessly, with each element pointing to the next. Lists are better when we frequently insert or remove elements in the middle.

Limitation 3: Slow Insertion and Deletion

Inserting an element in the middle of an array requires shifting all subsequent elements to make room — an O(n) operation. Deleting similarly requires shifting elements to close the gap. For applications that require frequent insertion and deletion, other structures (linked lists, trees) are more appropriate.

Limitation 4: No Key-Based Lookup

Arrays are indexed by position, not by key. If we want to look up a student's grade by their name, we must search through the array. Later, we will learn about associative structures that allow direct lookup by key.

For Now: Arrays Are Your Workhorse

Despite these limitations, arrays are the right choice for the majority of collection problems you will encounter as a learner. They are simple, efficient, and well-supported by the language. Master arrays thoroughly before moving on to more complex structures — the patterns you learn here (iteration, searching, accumulating) transfer directly to every other data structure.

A Decision Framework

When should you use an array? Ask these questions:

  1. Are all elements the same type? If yes, an array works. If no, you need records (Chapter 10) or some combination.
  2. Do you know the maximum size? If yes, a static array is simplest. If no, use a dynamic array.
  3. Will you frequently insert or delete from the middle? If yes, consider linked structures (later chapters). If no (or if insertions happen only at the end), arrays are fine.
  4. Do you need to look up elements by a key (like a name)? If yes, you will eventually want a hash table or dictionary. If position-based access suffices, arrays are ideal.
  5. How large is the data? Arrays are memory-efficient — there is zero overhead per element (unlike linked lists, which need a pointer per node). For large collections of simple values, arrays are hard to beat.

For most problems in this course and in real-world programming, the answer is: start with an array. Move to a more complex structure only when you hit one of the specific limitations listed above.


9.9 Introducing GradeBook Pro

Let us put everything together with GradeBook Pro, an application that will serve as our anchor example throughout Part II. In this first version, we use parallel arrays to manage a simple gradebook.

The Problem

A teacher needs to: 1. Store names and grades for up to 30 students 2. Calculate the class average 3. Find the highest and lowest grades 4. Display an honor roll (students with grades 90 or above) 5. Show a grade distribution (how many A's, B's, C's, etc.)

The Design

We will use three parallel arrays:

const
  MAX_STUDENTS = 30;

type
  TNameArray  = array[1..MAX_STUDENTS] of String;
  TGradeArray = array[1..MAX_STUDENTS] of Integer;
  TAttendArray = array[1..MAX_STUDENTS] of Integer;  { days present }

A variable studentCount tracks how many students are actually enrolled (which may be fewer than MAX_STUDENTS).

Core Implementation

procedure ReadStudents(var names: TNameArray;
                       var grades: TGradeArray;
                       var count: Integer);
var
  name: String;
begin
  count := 0;
  Write('Enter student name (empty to stop): ');
  ReadLn(name);
  while (name <> '') and (count < MAX_STUDENTS) do
  begin
    Inc(count);
    names[count] := name;
    Write('Enter grade for ', name, ': ');
    ReadLn(grades[count]);
    Write('Enter student name (empty to stop): ');
    ReadLn(name);
  end;
end;

function ClassAverage(const grades: TGradeArray;
                       count: Integer): Real;
var
  i, total: Integer;
begin
  total := 0;
  for i := 1 to count do
    total := total + grades[i];
  if count > 0 then
    ClassAverage := total / count
  else
    ClassAverage := 0.0;
end;

procedure ShowHonorRoll(const names: TNameArray;
                         const grades: TGradeArray;
                         count: Integer);
var
  i: Integer;
  found: Boolean;
begin
  WriteLn;
  WriteLn('=== HONOR ROLL (90+) ===');
  found := False;
  for i := 1 to count do
    if grades[i] >= 90 then
    begin
      WriteLn('  ', names[i], ' — ', grades[i]);
      found := True;
    end;
  if not found then
    WriteLn('  (no students qualify)');
end;

procedure ShowDistribution(const grades: TGradeArray;
                            count: Integer);
var
  i: Integer;
  dist: array['A'..'F'] of Integer;
  ch: Char;
begin
  for ch := 'A' to 'F' do
    dist[ch] := 0;

  for i := 1 to count do
    case grades[i] of
      90..100: Inc(dist['A']);
      80..89:  Inc(dist['B']);
      70..79:  Inc(dist['C']);
      60..69:  Inc(dist['D']);
    else
      Inc(dist['F']);
    end;

  WriteLn;
  WriteLn('=== GRADE DISTRIBUTION ===');
  for ch := 'A' to 'F' do
    if ch <> 'E' then  { skip E — not used in letter grades }
      WriteLn('  ', ch, ': ', dist[ch]);
end;

Notice how the grade distribution uses a character-indexed array — dist['A'] through dist['F'] — making the code beautifully readable. The case statement maps numeric grades to letters (recall the spaced review question from Chapter 5!).

What GradeBook Pro Reveals

Even in this simple version, several design tensions emerge:

  1. Parallel arrays are fragile. The names and grades arrays must stay synchronized. If we sort by grade, we must simultaneously rearrange names. In Chapter 10, we will replace these parallel arrays with an array of records.

  2. Fixed capacity wastes memory. We allocate space for 30 students even if only 12 are enrolled. Chapter 10 will also show us how dynamic arrays of records can solve this.

  3. The algorithms are reusable. The ClassAverage, search, and distribution logic work on any array of grades — they are not specific to GradeBook Pro. This is the power of separating algorithms from specific applications.

GradeBook Pro will evolve in every chapter of Part II, gaining records, files, sorting, and eventually a complete menu-driven interface. What we build here is the foundation.

Tracing Through GradeBook Pro

Let us trace through a concrete example to make sure the logic is clear. Suppose Ms. Alvarez enters five students:

Index Name Grade
1 Alice Chen 95
2 Bob Martinez 78
3 Carol Washington 62
4 David Kim 88
5 Eva Johansson 91

ClassAverage: total = 95 + 78 + 62 + 88 + 91 = 414. Average = 414 / 5 = 82.8.

FindExtremes: Start with highIdx = 1 (grade 95), lowIdx = 1 (grade 95). - i=2: 78 < 95, so lowIdx = 2. 78 < 95, so highIdx unchanged. - i=3: 62 < 78, so lowIdx = 3. 62 < 95, so highIdx unchanged. - i=4: 88 > 62 but < 95, no changes. - i=5: 91 > 62 but < 95, no changes. - Result: highIdx = 1 (Alice, 95), lowIdx = 3 (Carol, 62).

ShowDistribution: The case statement maps each grade to a letter: - 95 maps to 'A' (90..100 range) - 78 maps to 'C' (70..79 range) - 62 maps to 'D' (60..69 range) - 88 maps to 'B' (80..89 range) - 91 maps to 'A' (90..100 range)

Result: A=2, B=1, C=1, D=1, F=0. This matches our intuition.

Tracing through algorithms with concrete data is one of the most important skills in programming. If you cannot trace it on paper, you do not yet understand the algorithm.


9.10 Project Checkpoint: PennyWise Arrays

It is time to upgrade our PennyWise personal expense tracker to use arrays. In previous chapters, PennyWise could process one expense at a time. Now we will store up to 100 expenses and calculate monthly totals.

Data Design

const
  MAX_EXPENSES = 100;

type
  { Parallel arrays for expense data }
  TAmountArray  = array[1..MAX_EXPENSES] of Real;
  TMonthArray   = array[1..MAX_EXPENSES] of Integer;  { 1..12 }
  TCategoryArray = array[1..MAX_EXPENSES] of String;

  { Monthly totals — naturally indexed 1..12 }
  TMonthlyTotals = array[1..12] of Real;

Core Functionality

The checkpoint adds these features to PennyWise:

  1. Store expenses: Read up to 100 expenses (amount, month, category) into parallel arrays.
  2. Monthly totals: Accumulate expenses by month into a TMonthlyTotals array.
  3. Find highest spending month: Apply FindMaxIndex to the monthly totals.
  4. Category breakdown: Count and total expenses in each category.
procedure CalculateMonthlyTotals(const amounts: TAmountArray;
                                  const months: TMonthArray;
                                  count: Integer;
                                  var totals: TMonthlyTotals);
var
  i, m: Integer;
begin
  for m := 1 to 12 do
    totals[m] := 0.0;

  for i := 1 to count do
    totals[months[i]] := totals[months[i]] + amounts[i];
end;

This function demonstrates a common pattern: using one array's values as indices into another. Each expense's month (1-12) serves as the index into the totals array. It is concise and efficient.

What We Deferred

PennyWise still uses parallel arrays, which we acknowledged are fragile. In Chapter 10, we will replace these with an array of TExpense records, grouping each expense's amount, month, and category into a single entity. For now, the parallel-array version teaches us the core patterns.

Design Decisions in PennyWise

Several design decisions in this checkpoint deserve explanation:

Why array[1..12] for monthly totals? Because months are naturally numbered 1 to 12. The index is the month number, making the code self-documenting. We could have used 0..11 and added 1 when displaying, but why add confusion?

Why array[1..100] for expenses? This is the partially filled array pattern. We picked 100 as a reasonable upper bound for a simple expense tracker. The expCount variable tracks how many slots are actually used. In a production application, we might use a dynamic array to remove the limit entirely.

Why parallel arrays instead of records? We have not learned records yet! This is a pedagogical choice — we use the tools we have, and we will refactor in the next chapter. This also gives us firsthand experience with the fragility of parallel arrays, making the motivation for records feel earned rather than abstract.

Why separate the calculation from the display? CalculateMonthlyTotals computes the totals; a separate procedure displays them. This separation of computation from presentation is a fundamental software design principle. It means we could later display the same data in a different format (a bar chart, a CSV export, a GUI window) without changing the calculation logic.

Running PennyWise

With the array-based version, a typical session looks like:

=== PennyWise Expense Tracker ===

Enter expense (0 to stop):
  Amount: 45.50
  Month (1-12): 3
  Category: Groceries

Enter expense (0 to stop):
  Amount: 120.00
  Month (1-12): 3
  Category: Utilities

Enter expense (0 to stop):
  Amount: 35.00
  Month (1-12): 4
  Category: Groceries

Enter expense (0 to stop):
  Amount: 0

=== Monthly Totals ===
  January:   $0.00
  February:  $0.00
  March:     $165.50
  April:     $35.00
  ...

Highest spending month: March ($165.50)
Total expenses: $200.50
Number of expenses: 3

9.11 Chapter Summary

Arrays are the first true data structure in our Pascal journey, and mastering them is essential for everything that follows. Let us review what we learned:

Core Concepts: - An array stores a fixed number of elements of the same type in contiguous memory, providing O(1) access by index. - Pascal allows arrays indexed by any ordinal type — integers, characters, enumerated types, and subranges. - Named array types are essential for type compatibility when passing arrays to subprograms.

Working with Arrays: - Always initialize array elements before reading them. - Use Low, High, and Length for robust, maintainable loops. - Enable range checking ({$R+}) during development to catch out-of-bounds errors.

Subprogram Parameters: - Use var when the subprogram modifies the array. - Use const when the subprogram only reads the array (safer and potentially faster). - Use open array parameters (array of T) for functions that work on arrays of any size.

Algorithms: - Linear search, accumulation (sum/average), finding max/min, counting, and reversing are the fundamental array algorithms. - These patterns recur in every data structure we will study.

Multidimensional Arrays: - 2D arrays are declared with two index ranges and accessed with array[row, col]. - Pascal stores them in row-major order; iterate with the column index varying fastest for best performance.

Dynamic Arrays: - Declared without bounds (array of T), sized with SetLength, always zero-indexed. - Useful when size is unknown at compile time, but require careful memory management. - Resize in chunks rather than one element at a time to avoid O(n^2) copying costs.

Looking Ahead: In Chapter 10, we will introduce records — Pascal's mechanism for grouping related data of different types. Records will transform our parallel arrays into clean, maintainable data structures. GradeBook Pro will evolve from three fragile parallel arrays into a single array of TStudent records, and PennyWise will gain an array of TExpense records. The combination of arrays and records is one of the most powerful and widely used patterns in all of programming.


Spaced Review: Revisiting Earlier Concepts

Before moving on, let us reinforce connections to previous chapters:

From Chapter 7 (Procedures and Functions): What is the difference between a procedure and a function?

A procedure performs an action and does not return a value — it is called as a standalone statement. A function computes and returns a value — it is used within an expression. In this chapter, we saw both: FillWithZeros is a procedure (it modifies the array in place), while FindMax is a function (it returns the maximum value). The distinction becomes especially important with arrays, because modifying an array (procedure with var) is conceptually different from computing a value from it (function with const).

From Chapter 5 (Selection Statements): Write a CASE statement that maps integers 1-5 to letter grades A-F.

case score of
  5: grade := 'A';
  4: grade := 'B';
  3: grade := 'C';
  2: grade := 'D';
  1: grade := 'F';
end;

We used a similar case statement in GradeBook Pro's grade distribution, mapping numeric ranges (90-100, 80-89, etc.) to letter grades. The case statement and arrays work beautifully together.

From Chapter 3 (Data Types): What type would you use to store a count of items that will never be negative?

A Word (unsigned 16-bit, 0 to 65535) or Cardinal (unsigned 32-bit) is ideal for non-negative counts. However, in practice, many Pascal programmers use Integer for loop counters and counts because for loops and array indices work most naturally with signed integers. The key insight is that choosing the right type documents your intent — and that principle now extends to choosing the right array index range.


Key Vocabulary

Term Definition
Array A fixed-size collection of elements of the same type, stored contiguously and accessed by index
Index (Subscript) The ordinal value that identifies a specific element's position in an array
Contiguous Stored in adjacent memory locations with no gaps
Open array parameter A procedure/function parameter declared as array of T that accepts arrays of any size
Static array An array whose size is fixed at compile time
Dynamic array An array whose size can be changed at runtime with SetLength
Parallel arrays Multiple arrays where corresponding indices refer to the same logical entity
Row-major order Memory layout where elements of the same row are stored consecutively
Linear search Examining each element sequentially until the target is found or the array is exhausted
Bounds checking Runtime verification that an array index falls within the declared range

In the next chapter, we will discover how records let us group related data into a single entity — and how arrays of records combine the best of both worlds.