Command Palette

Search for a command to run...

MTH 4300

Lecture 6

C-Style Arrays

An array is a fixed-size, contiguous block of elements all of the same type. You declare one by specifying the type, name, and size:

Array declaration
int scores[5];                  // 5 uninitialized ints
int primes[4] = {2, 3, 5, 7};  // initialized at declaration

Array indexing is zero-based. primes[0] is the first element, primes[3] is the last.

Accessing elements
int primes[4] = {2, 3, 5, 7};
std::cout << primes[0] << "\n"; // 2
std::cout << primes[3] << "\n"; // 7

C++ does not check array bounds. Accessing primes[4] or primes[-1] is undefined behavior — no error, no exception, just silent data corruption or a crash.

Arrays and pointers

In Lecture 4 we used pointer arithmetic to walk through an array. That works because an array name in C++ decays to a pointer to its first element.

Array decay
int nums[3] = {10, 20, 30};
int* ptr = nums; // same as &nums[0]

std::cout << *ptr << "\n";       // 10
std::cout << *(ptr + 1) << "\n"; // 20
std::cout << ptr[2] << "\n";     // 30 — bracket notation works on pointers too

ptr[i] is exactly *(ptr + i) — the two notations are interchangeable.

The array name itself is not a pointer variable — you can't reassign it (nums = ptr is a compile error). But it converts to a pointer whenever a pointer is expected.

Passing arrays to functions

When you pass an array to a function, it decays to a pointer and the size information is lost. You have to track the size separately:

Passing an array
#include <iostream>

void print_all(int* arr, int size) {
    for (int i = 0; i < size; i++) {
        std::cout << arr[i] << " ";
    }
    std::cout << "\n";
}

int main() {
    int nums[4] = {1, 2, 3, 4};
    print_all(nums, 4);

    return 0;
}

Dynamic Allocation: new and delete

Stack variables live and die with their enclosing function. Sometimes you need memory that:

  • outlives the function that created it, or
  • has a size you don't know until runtime

For those cases, you allocate on the heap using new.

Allocating a single object
#include <iostream>

int main() {
    int* p = new int;    // allocates one int on the heap
    *p = 42;
    std::cout << *p << "\n"; // 42

    delete p;            // release the heap memory
    p = nullptr;         // prevent accidental reuse

    return 0;
}

new returns a pointer to the allocated memory. delete releases it. After delete, the pointer is a dangling pointer — it points to freed memory. Setting it to nullptr immediately is defensive practice.

Memory leaks

If you lose the pointer before calling delete, the memory is never freed — that's a memory leak. The program won't crash immediately, but long-running programs can exhaust available memory.

int* p = new int(42);
p = nullptr; // leak: no pointer to the allocation remains

Modern C++ has smart pointers (std::unique_ptr, std::shared_ptr) that call delete automatically when they go out of scope, eliminating most manual memory management. They're beyond the scope of this course, but worth knowing they exist.

Dynamic arrays: new[] and delete[]

To allocate an array whose size is known only at runtime, use new[]:

Dynamic array
#include <iostream>

int main() {
    int n = 5; // could come from user input
    int* arr = new int[n];

    for (int i = 0; i < n; i++) {
        arr[i] = i * 10;
    }

    for (int i = 0; i < n; i++) {
        std::cout << arr[i] << " "; // 0 10 20 30 40
    }
    std::cout << "\n";

    delete[] arr; // must use delete[], not delete
    arr = nullptr;

    return 0;
}

delete frees a single object. delete[] frees an array. Mixing them is undefined behavior.

Common mistake
int* arr = new int[5];
delete arr;   // WRONG: use delete[] for arrays
delete[] arr; // CORRECT

Multi-Dimensional Arrays

Stack-allocated (fixed size)

A 2D array on the stack is declared with two size brackets:

2D stack array
int matrix[3][4]; // 3 rows, 4 columns
matrix[0][0] = 1; // row 0, column 0
matrix[2][3] = 9; // row 2, column 3

Elements are stored row-major in memory — all elements of row 0, then all of row 1, and so on:

Memory layout of int matrix[2][3]
┌─────────┬────────┬───────┐
│ Element │ Offset │       │
├─────────┼────────┼───────┤
│ [0][0]  │ +0     │ row 0 │
│ [0][1]  │ +4     │       │
│ [0][2]  │ +8     │       │
│ [1][0]  │ +12    │ row 1 │
│ [1][1]  │ +16    │       │
│ [1][2]  │ +20    │       │
└─────────┴────────┴───────┘

Heap-allocated (dynamic size)

When dimensions are only known at runtime, the standard approach is an array of pointers — each pointing to one row:

Dynamic 2D array
#include <iostream>

int main() {
    int rows = 3;
    int cols = 4;

    int** matrix = new int*[rows]; // array of row pointers
    for (int i = 0; i < rows; i++) {
        matrix[i] = new int[cols]; // each row is its own heap allocation
    }

    matrix[1][2] = 42; // same bracket syntax as a stack 2D array

    // cleanup: delete each row first, then the array of pointers
    for (int i = 0; i < rows; i++) {
        delete[] matrix[i];
    }
    delete[] matrix;
    matrix = nullptr;

    return 0;
}

The cleanup order matters: delete each row before deleting the array of row pointers. Reversing the order leaves the row allocations unreachable — a memory leak.

Exercises

Q1.

Without running the code, predict what this program prints. Then verify.

#include <iostream>

int main() {
    int vals[5] = {10, 20, 30, 40, 50};
    int* ptr = vals;

    std::cout << vals[2] << "\n";
    std::cout << *(ptr + 4) << "\n";
    std::cout << ptr[1] << "\n";

    ptr++;
    std::cout << *ptr << "\n";

    return 0;
}

Q2.

Without running the code, predict what this program prints.

#include <iostream>

int main() {
    int* p = new int(7);
    std::cout << *p << "\n"; // line A

    *p = *p * 3;
    std::cout << *p << "\n"; // line B

    delete p;
    p = nullptr;

    std::cout << (p == nullptr ? "null" : "not null") << "\n"; // line C

    return 0;
}

Q3.

Trace through this program step by step. At each checkpoint, describe what is in memory.

#include <iostream>

int main() {
    int n = 3;
    int* arr = new int[n];

    for (int i = 0; i < n; i++) {
        arr[i] = (i + 1) * 10;
    }
    // CHECKPOINT A: what does arr contain?

    arr[1] = arr[0] + arr[2];
    // CHECKPOINT B: what does arr contain?

    delete[] arr;
    arr = nullptr;
    // CHECKPOINT C: what is arr?

    return 0;
}

Q4.

Each snippet below has a bug. For each one, identify what's wrong and what the consequence is.

// Snippet A
int arr[3] = {1, 2, 3};
std::cout << arr[5] << "\n";

// Snippet B
int* p = new int(10);
p = nullptr;

// Snippet C
int* arr2 = new int[4];
delete arr2;

// Snippet D
int* p2 = new int(5);
delete p2;
delete p2;

Q5.

Write a function make_range(int n) that allocates an int array of size n on the heap, fills it with values 0, 1, 2, ..., n-1, and returns the pointer. The caller is responsible for freeing the memory. In main, call make_range(5), print all elements, then free the array.

#include <iostream>

int* make_range(int n) {
    // TODO
}

int main() {
    int* range = make_range(5);

    // TODO: print all 5 elements
    // TODO: free the memory

    return 0;
}

Q6.

Write a function make_identity(int n) that allocates an n × n identity matrix on the heap (1s on the diagonal, 0s elsewhere) and returns it as an int**. In main, call it with n = 3, print the matrix row by row, then free all memory in the correct order.

#include <iostream>

int** make_identity(int n) {
    // TODO
}

int main() {
    int n = 3;
    int** matrix = make_identity(n);

    // TODO: print the matrix (each row on its own line)
    // TODO: free the memory in the correct order

    return 0;
}

Practice Problems