Two-Dimensional Dynamic Arrays in C++, CSC01A1 Practical 8 – Study Notes
offline

Difficulty: Intermediate | Prerequisites: Pointers, single-dimensional arrays, basic C++ classes, separate compilation (header/.cpp files).

Big Picture

Up to this point in CSC01A1, you have worked with one-dimensional arrays and pointers. This practical steps it up: you allocate a grid of memory at runtime using a pointer-to-pointer pattern, which is how C++ handles two-dimensional arrays whose size is not known at compile time. Understanding this pattern is essential because real programs (games, simulations, image processing, matrices) routinely need grid-shaped data. If you are comfortable with new, delete, and single pointers, you have what you need to start here.


TL;DR

A two-dimensional dynamic array in C++ is built by allocating an array of pointers (the rows), then allocating a separate array of elements for each row. You access cells with grid[row][col]. Deallocation reverses the process: delete each row array first, then delete the array of pointers. Getting this order wrong causes memory leaks.


Key Terms

Dynamic array

An array whose size is determined at runtime rather than compile time, allocated on the heap using new. In simple terms, it is an array you create while the program is already running, based on user input or some other condition.

Pointer-to-pointer (double pointer)

A variable declared as int** grid that stores the address of an array of pointers. Think of it as a handle that points to a row of handles, where each of those handles points to one row of actual data.

Heap allocation

Memory obtained at runtime via new (or malloc in C). Unlike stack memory, heap memory persists until you explicitly free it with delete or delete[]. In simple terms, you are asking the operating system for a chunk of memory and promising to give it back when you are done.

Memory leak

A bug where allocated heap memory is never freed because the program loses the pointer to it. The memory stays reserved for the life of the process but can never be used or recovered. Think of it as leaving a hotel room booked and losing the key: the room is unavailable but nobody is in it.

delete[]

The C++ operator used to free memory that was allocated with new[] (an array allocation). Using plain delete on an array allocation is undefined behaviour.

Namespace

A named scope that groups related identifiers (functions, classes, variables) to prevent name collisions. In this practical, all game logic lives inside namespace ChaseSpace.

Separate compilation

Splitting a program into header files (.h or .hpp, containing declarations) and implementation files (.cpp, containing definitions). The compiler processes each .cpp file independently, and the linker combines them. This keeps large projects manageable and speeds up rebuilds.

Command-line arguments

Values passed to the program when it starts, received through int argc (argument count) and char* argv[] (argument vector) in main. In this practical, they supply the grid size, initial energy, number of targets, and number of obstacles.


Core Content

Allocating a 2D Dynamic Array

The standard C++ pattern uses two steps.

  • Step 1: Allocate the row pointers. Create an array of pointers, one per row:

    • char** grid = new char*[rows];

    • Each element of this array will point to one row of the grid.

  • Step 2: Allocate each row. Loop through the row-pointer array and allocate a column-length array for each:

    • for (int i = 0; i < rows; i++) { grid[i] = new char[cols]; }

    • Each grid[i] now points to its own block of cols elements on the heap.

After both steps, you can read and write individual cells with grid[row][col].

Initialising the Grid

Once allocated, every cell should be set to a known value before anything else touches it. In the CHASER game, the initialisation order matters:

  • Fill the entire grid with the "empty" marker (. or a space character) first.

  • Place obstacles (#) at random positions.

  • Place targets (T) at random positions, checking that the cell is not already occupied.

  • Place the player (P) at a random row in the first column (column 0), again checking the cell is empty.

This order prevents two objects from landing on the same cell. Each placement checks grid[r][c] before writing.

Deallocating a 2D Dynamic Array

Deallocation reverses allocation, inside-out:

  • Step 1: Delete each row. Loop through the row-pointer array and call delete[] grid[i]; for every row.

  • Step 2: Delete the row-pointer array itself. Call delete[] grid;.

If you skip step 1 and jump straight to delete[] grid, every row array leaks. If you delete the outer array first, you lose the pointers to the rows and can never free them.

Accessing Elements

With a pointer-to-pointer layout, grid[r] dereferences the outer pointer to get the address of row r, then grid[r][c] dereferences that row pointer to reach column c. The two bracket operations are two separate pointer dereferences, not a single offset calculation as in a statically declared 2D array.

Command-Line Arguments

The main function receives arguments as strings. To use them as integers you convert with atoi(argv[index]) or, in modern C++, std::stoi(argv[index]). Typical argument order for this practical:

  • argv[1]: number of rows

  • argv[2]: number of columns

  • argv[3]: initial energy (must be greater than 1)

  • argv[4]: number of targets

  • argv[5]: number of obstacles

Always check argc before accessing argv elements to avoid out-of-bounds reads.

Separate Compilation and Namespaces

The practical requires splitting the code into at least two compilation units:

  • A header file (e.g. ChaseSpace.h) declaring function prototypes and any shared types inside namespace ChaseSpace { ... }.

  • An implementation file (e.g. ChaseSpace.cpp) defining those functions, also inside namespace ChaseSpace { ... } or using ChaseSpace::functionName.

  • A driver file (main.cpp) that includes the header and calls the functions.

The namespace prevents your function names from colliding with names in the standard library or other code.


Formulas / Code Patterns

Allocation pattern:

char** grid = new char*[rows];
for (int i = 0; i < rows; i++)
    grid[i] = new char[cols];

Deallocation pattern:

for (int i = 0; i < rows; i++)
    delete[] grid[i];
delete[] grid;

Converting command-line arguments:

int rows = atoi(argv[1]);
int cols = atoi(argv[2]);

Real-World Applications

Two-dimensional dynamic arrays are the backbone of any program that works with grids: image processing (each pixel is a cell), game boards, spreadsheet engines, and scientific simulations of heat flow or fluid dynamics. The allocate-use-deallocate discipline you practise here is exactly what production C++ code does when managing large buffers whose size depends on user input or file contents.


Common Misconceptions

  • Students often think delete[] grid frees everything. It does not. It only frees the outer array of pointers. Each row was allocated separately and must be deleted separately first.

  • Students sometimes use delete (without the brackets) on an array allocated with new[]. This is undefined behaviour; always match new[] with delete[].

  • Some students assume a 2D dynamic array is stored contiguously in memory like a static 2D array. It is not. Each row can be at a completely different memory address.

  • A common mistake is forgetting to initialise cells before reading them. Uninitialized heap memory contains garbage values, and reading it causes unpredictable behaviour.


Why It Matters / Exam Flags

⚠️ Memory allocation and deallocation together carry 20 marks on this practical. If your program leaks memory, you lose those marks entirely.

⚠️ The order of deallocation (rows first, then the outer pointer array) is a classic exam question. Be able to write it from memory.

⚠️ Separate compilation (header + .cpp) carries 10 marks. If everything is in one file, those marks are gone.

⚠️ Command-line argument handling carries 10 marks. Hardcoding grid dimensions or energy values means zero for that section.

⚠️ Any submission that does not compile is capped at 40%.


Quick Self-Test

  1. True or false: delete[] grid is sufficient to free all memory used by a 2D dynamic array. (False, you must delete each row first.)

  1. Fill in the blank: The outer array in a 2D dynamic array stores ______, not data values. (Pointers to row arrays.)

  1. True or false: Each row of a pointer-to-pointer 2D array is guaranteed to be contiguous with the next row in memory. (False.)

  1. Fill in the blank: Command-line arguments arrive as ______ and must be converted to integers before arithmetic. (Strings / C-strings / char arrays.)

  1. True or false: Placing all your code in main.cpp with no header file satisfies the separate compilation requirement. (False.)


Practice Q&A

Q: Write the code to allocate a 2D dynamic array of char with r rows and c columns.

A: char** grid = new char*[r]; for (int i = 0; i < r; i++) grid[i] = new char[c];

Q: Write the code to correctly deallocate the array from the previous question.

A: for (int i = 0; i < r; i++) delete[] grid[i]; delete[] grid;

Q: What happens if you call delete[] grid without first deleting each row?

A: The row arrays remain allocated on the heap with no pointer referencing them. This is a memory leak.

Q: Why must the player be placed after obstacles and targets during initialisation?

A: Because the placement checks whether a cell is empty before writing. If the player were placed first, an obstacle or target could overwrite it (or an extra check would be needed). Placing the player last, in the first column, guarantees it lands on an unoccupied cell.

Q: Explain the difference between delete and delete[].

A: delete frees a single object allocated with new. delete[] frees an array of objects allocated with new[]. Using the wrong form is undefined behaviour.


Connections to Other Topics

This connects to pointers (Practical 6/7) because a 2D dynamic array is just pointers all the way down. It also previews how containers like std::vector solve the same problem with automatic memory management, which you will likely encounter in later modules. The separate compilation requirement ties directly to software engineering principles around modularity and encapsulation.


Related Terms / Search Tags

2D dynamic array, two-dimensional array C++, pointer to pointer, double pointer, new delete array, dynamic memory allocation C++, heap allocation, memory leak C++, delete vs delete[], separate compilation C++, header file .h .hpp, namespace C++, command-line arguments argc argv, atoi stoi, grid data structure, matrix allocation C++, CSC01A1 Practical 8, ChaseSpace namespace