Skip to content

Pointers and Free Store

Video: you will never ask about pointers again after watching this video

Pointers are one of the most powerful and dangerous features of C++. They give you direct control over memory, but with great power comes great responsibility.

What is a Pointer?

A pointer is a variable that stores an address in memory:

cpp
int x = 42;
int* ptr = &x;  // ptr stores the address of x

std::cout << "Value of x: " << x << std::endl;      // 42
std::cout << "Address of x: " << &x << std::endl;   // 0x7fff5fbff8ac
std::cout << "Value of ptr: " << ptr << std::endl;  // 0x7fff5fbff8ac
std::cout << "Value ptr points to: " << *ptr << std::endl; // 42

Key concepts:

  • & - Address-of operator (gets the address of a variable)
  • * - Dereference operator (gets the value at an address)
  • int* - Pointer to int type

nullptr - The Modern Null Pointer

In modern C++, use nullptr instead of NULL or 0:

cpp
int* ptr1 = nullptr;  // Modern C++ way
int* ptr2 = NULL;     // Old C way (avoid)
int* ptr3 = 0;        // Also old way (avoid)

if (ptr1 == nullptr) {
    std::cout << "Pointer is null" << std::endl;
}

Why nullptr is better:

  • Type-safe (can't be implicitly converted to int)
  • Clearer intent
  • Better for function overloading

Stack vs Free Store (Heap)

C++ has two main memory areas:

Stack Memory

cpp
void function() {
    int x = 42;        // Stack allocation
    double y = 3.14;   // Stack allocation
    // x and y are automatically deallocated when function ends
}

Stack characteristics:

  • Automatic allocation/deallocation
  • Limited size (typically a few MB)
  • Fast access
  • LIFO (Last In, First Out) order

Each function call creates a new stack frame with space allocated for local variable and function parameters. The size of the stack frame is determined by the compiler based on the function's signature and local variables.

Any variables you declare inside a function are allocated on the stack. When the function returns, the stack frame is popped and the variables are deallocated.

For example:

cpp
void func1() {
    int x = 10;
    func2();
}

void func2() {
    int y = 20;
    // x and y are deallocated when func2 returns
}

When func2 is called, the stack may look something like this:

cpp
[y] # func2_frame 
[return address to f1]
[x] # func1_frame
[return address to main]

Free Store (Heap) Memory

cpp
void function() {
    int* ptr = new int(42);  // Free store allocation
    // ptr must be manually deallocated with delete
    delete ptr;  // Manual deallocation
}

Free store characteristics:

  • Manual allocation/deallocation
  • Much larger size (limited by system memory)
  • Slower access
  • No specific order

It is possible for the programmer to tell C++: I'll manage this memory manually, I'll allocate and deallocate it myself. This is the free store. C++ then completely transfers memory management from itself to the programmer. While some languages like Java, Python, and JavaScript have a garbage collector, C++ does not. The language requires you to explicitly deallocate any memory that you allocated.

This technique is useful when it is simply impossible for the compiler to know the size of the memory you need to allocate. For example, if you want to create a dynamically sized array. There is no way for the compiler to know the size of the array, because a user can pass in any size they want. However, the programmer can use new/delete to allocate and deallocate memory as needed.

Dynamic Memory Allocation

new and delete

cpp
// Single object allocation
int* ptr = new int(42);
std::cout << *ptr << std::endl;  // 42
delete ptr;  // Must delete what you new

// Array allocation
int* arr = new int[5]{1, 2, 3, 4, 5};
for (int i = 0; i < 5; ++i) {
    std::cout << arr[i] << " ";
}
deletearr;  // Use deletefor arrays

Common Patterns

cpp
// Allocate and initialize
int* ptr = new int(42);

// Allocate array
int* arr = new int[10];

// Allocate with default initialization
int* ptr2 = new int();  // Initialized to 0

// Allocate without initialization (undefined value)
int* ptr3 = new int;    // Uninitialized!

Ownership Concepts

Ownership is a crucial concept in C++ memory management:

cpp
class ResourceManager {
private:
    int* data;

public:
    // Constructor - takes ownership
    ResourceManager(int size) : data(new int[size]) {}

    // Destructor - releases ownership
    ~ResourceManager() {
        deletedata;  // Clean up what we own
    }

    // Copy constructor - creates new ownership
    ResourceManager(const ResourceManager& other) {
        data = new int[/* size */];
        // Copy data...
    }

    // Assignment operator - transfers ownership
    ResourceManager& operator=(const ResourceManager& other) {
        if (this != &other) {
            deletedata;  // Release old ownership
            data = new int[/* size */];
            // Copy data...
        }
        return *this;
    }
};

Ownership rules:

  • Whoever allocates memory owns it
  • Owner is responsible for deallocation
  • Only one owner at a time (unless shared ownership)
  • Transfer ownership explicitly

Common Memory Errors

1. Dangling Pointers

cpp
int* createDanglingPointer() {
    int x = 42;
    return &x;  // DANGER! x goes out of scope
}

int main() {
    int* ptr = createDanglingPointer();
    std::cout << *ptr << std::endl;  // Undefined behavior!
    return 0;
}

Fix: Don't return pointers to local variables.

2. Double Delete

cpp
int* ptr = new int(42);
delete ptr;   // OK
delete ptr;   // DANGER! Double delete - undefined behavior

Fix: Set pointer to nullptr after delete:

cpp
int* ptr = new int(42);
delete ptr;
ptr = nullptr;  // Safe to delete nullptr
delete ptr;     // Safe (does nothing)

3. Memory Leaks

cpp
void memoryLeak() {
    int* ptr = new int(42);
    // Forgot to delete ptr!
    // Memory leak - ptr goes out of scope, memory is lost
}

Fix: Always delete what you new, or use smart pointers.

4. Exception Between new and delete

cpp
void riskyFunction() {
    int* ptr = new int(42);
    someFunctionThatMightThrow();  // If this throws...
    delete ptr;  // This never executes!
}

Fix: Use smart pointersSmart pointers add ownership semantics to pointers and automatically nanage deletion for you or try-catchtry-catch are used to handle exceptions and ensure resources are cleaned up properly, both discussed in further articles.

Summary

You've learned:

  • How pointers work and how to use them safely
  • The difference between stack and free store memory
  • How to allocate and deallocate memory with new/delete
  • Common memory errors and how to avoid them

Key takeaways:

  • Always delete what you new
  • Use smart pointers when possible
  • Understand ownership and transfer it explicitly
  • Initialize pointers to nullptr

Your Task

Implement a simple dynamic array class called 'DynamicArray' that manages its own memory. The class should support:

  1. Constructor that takes initial capacity
  2. Destructor to free memory
  3. push_back() method to add elements
  4. getSize() method to return current size
  5. getCapacity() method to return current capacity
  6. get(index) method for element access
  7. clear() method to remove all elements
  8. empty() method to check if array is empty

Handle memory allocation failures and ensure no memory leaks. This will test your understanding of new/delete, exception safety, and proper memory management.

Implement a DynamicArray class with constructor, destructor, push_back, getSize, getCapacity, get, clear, and empty methods.

cpp
class DynamicArray {
private:
    int* data;
    size_t size;
    size_t capacity;
public:
    DynamicArray(size_t initialCapacity = 10) {
        // - Allocate memory for initialCapacity elements
        // - Initialize size to 0, capacity to initialCapacity
        // - Handle the case where initialCapacity is 0
    }
    ~DynamicArray() {
        // - Delete the allocated array
        // - Set data to nullptr
    }
    void push_back(int value) {
        // - Check if we need to resize (size >= capacity)
        // - If resize needed: allocate new array with 2x capacity, copy elements, delete old array
        // - Add the new element at the end
        // - Increment size
    }
    size_t getSize() const {
    }
    size_t getCapacity() const {
    }
    int& get(size_t index) {
        // - Check bounds (index < size)
        // - Return reference to element at index
        // - Throw std::out_of_range if index is invalid
    }
    void clear() {
        // - Set size to 0
        // - Don't deallocate memory (keep capacity)
    }
    bool empty() const {
        // - Return true if size is 0, false otherwise
    }
};