Skip to main content

📝 Lesson 8: Pointers and Dynamic Memory

Unlocking direct memory access and dynamic resource management in C++.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Understand memory addresses and the role of pointers as direct memory references.
  • Master pointer declaration, dereferencing, and pointer arithmetic.
  • Implement dynamic memory allocation using new and delete.
  • Identify and prevent common memory pitfalls like leaks, dangling pointers, and double-deletions.

Estimated Time: 90–120 minutes

Project: Build a dynamic student database that manages variable-sized arrays of student records using pointers.

In This Lesson

What Are Pointers?

Imagine your computer's memory as a huge apartment building. Each apartment (memory location) has a unique address. A pointer is like having someone's address written on a piece of paper - it tells you WHERE to find them, not WHO they are!

Understanding Memory Addresses

Every variable lives somewhere in memory. The & operator (address-of) tells you WHERE it lives!

Variables and Their Addresses Variable int age = 25; Value: 25 &age Address: 0x7ffe5c Pointer int* ptr = &age; Value: 0x7ffe5c *ptr Points to: 25

Pointer Declaration and Usage

graph TD A[Pointer Operations] --> B["Declaration: type* name"] A --> C["Address-of: &variable"] A --> D["Dereference: *pointer"] B --> E["int* ptr;"] C --> F["ptr = &x;"] D --> G["value = *ptr;"] E --> H[Creates pointer variable] F --> I[Gets address of x] G --> J[Gets value at address]

Basic Pointer Examples

// Pointer basics
int x = 42;           // Regular variable
int* ptr;             // Pointer to int (currently uninitialized - dangerous!)
ptr = &x;             // ptr now holds the address of x

cout << "x = " << x << endl;              // Prints: 42
cout << "&x = " << &x << endl;            // Prints: address (e.g., 0x7ffe5c)
cout << "ptr = " << ptr << endl;          // Prints: same address
cout << "*ptr = " << *ptr << endl;        // Prints: 42

// Modifying through pointer
*ptr = 100;           // Changes the value at the address ptr points to
cout << "x = " << x << endl;              // Prints: 100 (x was changed!)

// Multiple pointers to same variable
int* ptr2 = &x;
*ptr2 = 200;
cout << "*ptr = " << *ptr << endl;        // Prints: 200 (both see the change)

Pointer Arithmetic: Navigation in Memory

Pointer arithmetic is like navigating apartments - adding 1 to a pointer moves to the next apartment of that size!

Pointer Arithmetic Examples

// Array and pointer relationship
int arr[] = {10, 20, 30, 40, 50};
int* ptr = arr;  // ptr points to first element

// Different ways to access elements
cout << arr[0] << endl;      // 10
cout << *ptr << endl;        // 10 (same as arr[0])
cout << *(ptr + 2) << endl;  // 30 (same as arr[2])

// Moving through array with pointer
for (int i = 0; i < 5; i++) {
    cout << *ptr << " ";     // Print current element
    ptr++;                   // Move to next element
}

// Pointer arithmetic with different types
char str[] = "Hello";
char* cptr = str;
cptr++;  // Moves 1 byte (size of char)

double values[] = {1.1, 2.2, 3.3};
double* dptr = values;
dptr++;  // Moves 8 bytes (size of double)

Dynamic Memory Allocation: Creating Space at Runtime

Static memory is like assigned seating - you know exactly how many seats you need beforehand. Dynamic memory is like a restaurant that can add tables as guests arrive!

Static vs Dynamic Memory Stack (Static) int x = 10; int arr[5]; ✓ Size known at compile ✓ Automatic cleanup ✗ Fixed size ✗ Limited space Heap (Dynamic) int* p = new int; int* arr = new int[n]; ✓ Size at runtime ✓ Flexible size ✓ Large space ✗ Manual cleanup!

The new and delete Operators

stateDiagram-v2 [*] --> Dynamic_Memory_Management Dynamic_Memory_Management --> Allocation Dynamic_Memory_Management --> Deallocation state Allocation { [*] --> Choose_kind Choose_kind --> Single_object: use new type Choose_kind --> Array: use new with size N Single_object --> Example_single: int pointer = new int Array --> Example_array: int array of size 10 } state Deallocation { [*] --> Choose_delete Choose_delete --> Delete_single: delete ptr Choose_delete --> Delete_array: delete array Delete_single --> Example_del_single: delete ptr Delete_array --> Example_del_array: delete array }

Dynamic Memory Examples

// Single variable allocation
int* ptr = new int;        // Allocate space for one int
*ptr = 42;                 // Use it like normal
cout << *ptr << endl;      // Prints: 42
delete ptr;                // FREE THE MEMORY!

// Array allocation
int size;
cout << "How many numbers? ";
cin >> size;

int* numbers = new int[size];  // Dynamic array!

// Use the array
for (int i = 0; i < size; i++) {
    numbers[i] = i * 10;
}

// Print array
for (int i = 0; i < size; i++) {
    cout << numbers[i] << " ";
}

delete[] numbers;          // FREE THE ARRAY! Note the []

// Common mistake - memory leak!
int* leak = new int;
leak = new int;            // Old memory is lost forever!
// Always delete before reassigning!

🧭 Modern C++ note: prefer RAII over raw new/delete

You are learning raw new/delete because they are the foundation everything else is built on — but in real modern C++ code you should almost never write an owning new/delete by hand. Every raw new is a promise to call delete on exactly one path, and a single early return or thrown exception breaks that promise and leaks. Instead, let a resource-owning object free the memory automatically when it goes out of scope — the pattern called RAII (Resource Acquisition Is Initialization). The standard smart pointer std::unique_ptr, created with std::make_unique, does exactly this:

#include <memory>

auto ptr = std::make_unique<int>(42);  // no raw new
std::cout << *ptr << '\n';              // use it like a pointer
// no delete needed — memory is freed automatically at end of scope

We use raw pointers for the rest of this lesson so you understand what smart pointers do under the hood. You will meet them in full in the Smart Pointers lesson, and the RAII idea returns in Lesson 9: Classes and Objects through constructors and destructors.

Memory Leaks: The Silent Killer

Memory leaks are like borrowing books from a library and never returning them - eventually, there are no books left for anyone!

Pointer Safety Rules

Pointer Safety Guidelines Initialize Pointers ✗ int* ptr; ✓ int* ptr = nullptr; Check Before Use if (ptr != nullptr) { *ptr = 10; } Delete = Set nullptr delete ptr; ptr = nullptr; Match new/delete new → delete new[] → delete[] Don't Delete Twice ✗ delete ptr; ✗ delete ptr; // Crash!

Dynamic Arrays in Practice

// Dynamic array example - Grade management system
class GradeManager {
private:
    double* grades;
    int capacity;
    int size;

public:
    GradeManager(int initialCapacity = 10) {
        capacity = initialCapacity;
        size = 0;
        grades = new double[capacity];
    }

    ~GradeManager() {
        delete[] grades;  // Destructor cleans up!
    }

    void addGrade(double grade) {
        if (size == capacity) {
            // Need to grow the array
            capacity *= 2;
            double* newGrades = new double[capacity];

            // Copy old data
            for (int i = 0; i < size; i++) {
                newGrades[i] = grades[i];
            }

            // Delete old array
            delete[] grades;
            grades = newGrades;
        }

        grades[size++] = grade;
    }

    double getAverage() {
        if (size == 0) return 0;

        double sum = 0;
        for (int i = 0; i < size; i++) {
            sum += grades[i];
        }
        return sum / size;
    }
};

⚠️ Heads up: this class owns raw memory

Because GradeManager owns a raw new[] buffer, copying one (GradeManager b = a;) would copy the pointer, not the data — so two objects would delete[] the same memory and crash. A class that owns a raw resource needs a destructor and a copy constructor and copy assignment operator (the Rule of Three), which you'll cover in Lesson 9. In production code you'd skip all of this and use std::vector<double>, which grows, copies, and cleans up for you automatically.

Pointers and Functions

Pointers let functions modify variables directly and work with arrays efficiently!

Functions with Pointers

// Passing pointers to modify variables
void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// Array functions always receive pointers
int sumArray(int* arr, int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
        sum += arr[i];  // or *(arr + i)
    }
    return sum;
}

// Returning dynamically allocated memory
int* createArray(int size) {
    int* arr = new int[size];
    for (int i = 0; i < size; i++) {
        arr[i] = i * i;
    }
    return arr;  // Caller must delete[]!
}

// Safe string copy
void safeCopy(char* dest, const char* src, int maxLen) {
    int i = 0;
    while (src[i] != '\0' && i < maxLen - 1) {
        dest[i] = src[i];
        i++;
    }
    dest[i] = '\0';
}

Practice Exercise: Dynamic Student Records

🏋️ Build a Dynamic Grade List

Practice raw dynamic memory using only pointers, new[]/delete[], and functions — no classes yet (those are the next lesson).

Instructions:

  1. Keep two parallel dynamic arraysstring* names and double* grades — with a count and a capacity.
  2. When the arrays fill up, grow them: allocate a bigger array, copy the existing values over, and delete[] the old one.
  3. Compute and print the class average.
  4. Free both arrays with delete[] before the program ends.

Starter Code:

#include <iostream>
#include <string>
using namespace std;

int main() {
    int capacity = 2, count = 0;
    string* names  = new string[capacity];
    double* grades = new double[capacity];

    // TODO: add a few students, growing both arrays when count == capacity
    // TODO: print the average
    // TODO: delete[] names; delete[] grades;
    return 0;
}
💡 Hint

To grow an array: double* bigger = new double[newCap];, copy bigger[i] = old[i]; for every i < count, then delete[] old; and keep bigger. Do the same for names. When count == capacity, double the capacity before adding the next student.

✅ Solution
#include <iostream>
#include <string>
using namespace std;

double* growD(double* old, int count, int newCap) {
    double* bigger = new double[newCap];
    for (int i = 0; i < count; i++) bigger[i] = old[i];
    delete[] old;
    return bigger;
}
string* growS(string* old, int count, int newCap) {
    string* bigger = new string[newCap];
    for (int i = 0; i < count; i++) bigger[i] = old[i];
    delete[] old;
    return bigger;
}

int main() {
    int capacity = 2, count = 0;
    string* names  = new string[capacity];
    double* grades = new double[capacity];

    string inNames[]  = {"Ana", "Ben", "Cy"};
    double inGrades[] = {90.0, 82.5, 77.0};

    for (int i = 0; i < 3; i++) {
        if (count == capacity) {            // full — grow both arrays
            capacity *= 2;
            names  = growS(names,  count, capacity);
            grades = growD(grades, count, capacity);
        }
        names[count]  = inNames[i];
        grades[count] = inGrades[i];
        count++;
    }

    double sum = 0;
    for (int i = 0; i < count; i++) sum += grades[i];
    cout << "Class average: " << sum / count << '\n';

    delete[] names;
    delete[] grades;
    return 0;
}

🔭 Optional Stretch: Wrap It in a Class (peek ahead)

Peeks into the next lesson — skip it freely. All that manual new[]/delete[] and "grow when full" bookkeeping is exactly what a class is for: bundle the arrays, count, and capacity together, and let a destructor free the memory automatically (the pattern called RAII). You'll build this properly in Lesson 9: Classes and Objects — here's the shape it takes:

✅ Peek
struct Student { std::string name; double grade; };

class StudentDatabase {
    Student* students;
    int capacity, count;
public:
    StudentDatabase() : students(new Student[5]), capacity(5), count(0) {}
    ~StudentDatabase() { delete[] students; }   // frees automatically — RAII
    void addStudent(std::string name, double grade);   // grows when full
    double getClassAverage() const;
};

Common Pointer Pitfalls

graph TD A[Pointer Pitfalls] --> B[Dangling Pointer] A --> C[Memory Leak] A --> D[Buffer Overflow] A --> E[Double Delete] B --> F[Points to deleted memory] C --> G[Forget to delete] D --> H[Write past array end] E --> I[Delete same memory twice]

Smart Pointers Preview (Modern C++)

Modern C++ provides smart pointers that automatically manage memory - like having a responsible assistant!

Evolution to Smart Pointers Raw Pointer Manual management Error prone Smart Pointer Automatic cleanup Exception safe unique_ptr shared_ptr

The modern default is std::unique_ptr, created with std::make_unique — it owns the memory and frees it automatically, so you never write delete yourself:

#include <memory>

auto p = std::make_unique<int>(42);   // replaces: int* p = new int(42);
*p = 100;                             // use it just like a raw pointer
// automatically deleted when p goes out of scope — no leak possible

The full lesson on Smart Pointers covers unique_ptr, shared_ptr, and weak_ptr in depth. Once you've met them, reach for smart pointers first and keep raw pointers for non-owning "just look at it" references.

Challenge Exercise: Dynamic Matrix Operations

🏋️ Advanced Pointer Challenge

Work with a dynamic 2D array (a matrix) using only raw pointers, new/delete, and functions:

  1. Allocate a rows × cols grid of int using int**.
  2. Write functions to fill it and to print it.
  3. Add two same-sized matrices into a third.
  4. Free every row, then the outer array — no leaks.
💡 Hint
// Allocate 2D array
int** matrix = new int*[rows];
for(int i = 0; i < rows; i++) {
    matrix[i] = new int[cols];
}

// Don't forget to delete!
for(int i = 0; i < rows; i++) {
    delete[] matrix[i];
}
delete[] matrix;

🔭 Optional peek ahead: once you reach Lesson 9: Classes and Objects and Lesson 13: Operator Overloading, come back and wrap this in a Matrix class whose destructor frees the memory and whose operator+ adds two matrices with natural a + b syntax.

🎯 Quick Quiz

Question 1: Given int x = 5; and int* p = &x;, what does *p evaluate to?

Question 2: You allocated an array with int* a = new int[10];. How must you free it?

Question 3: What is a "dangling pointer"?

Summary

🎉 Key Takeaways

  • Pointers store memory addresses, not values
  • & gets the address of a variable
  • * dereferences a pointer to access the value
  • new allocates dynamic memory on the heap
  • delete frees allocated memory - always match with new!
  • Arrays decay to pointers when passed to functions
  • Initialize pointers to nullptr to avoid undefined behavior
  • Memory leaks occur when you forget to delete
  • Modern C++ prefers smart pointers over raw pointers
graph LR A[Master Pointers] --> B[Control Memory] B --> C[Build Efficient Programs] C --> D[Create Dynamic Applications] D --> E[Professional C++ Developer!]

📚 Additional Resources

🚀 What's Next?

You can now reach directly into memory and manage it yourself with raw new/delete. Next, in Lesson 9: Classes and Objects, you'll bundle data together with the behavior that operates on it — and put this new/delete discipline to work inside constructors and destructors through RAII.

🎉 Memory mastery unlocked!

Pointers are where a lot of learners stall — and you pushed through. Every efficient C++ program you write from here rests on what you just learned.