Memory Management

Pointers and References

Master C++ pointers and references — declaration, dereferencing, pointer arithmetic, nullptr, references vs pointers, void pointers, and function pointers.

Pointers and References

Pointers and references are how C++ programs work directly with memory addresses. Mastering them is essential — they underpin dynamic memory allocation, pass-by-reference semantics, polymorphism, and the entire STL iterator model. Modern C++ minimises raw pointer use through smart pointers and references, but you must understand the foundation.


Pointers

A pointer is a variable that stores a memory address. The * operator declares a pointer and dereferences it. The & operator takes the address of a variable:

cpp
#include <iostream>
using namespace std;

int main() {
    int x = 42;

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

    cout << x     << "\n";   // 42   — value of x
    cout << &x    << "\n";   // 0x... — address of x
    cout << ptr   << "\n";   // 0x... — same address stored in ptr
    cout << *ptr  << "\n";   // 42   — value at the address (dereference)

    // Modify x through the pointer
    *ptr = 100;
    cout << x << "\n";   // 100 — x was modified through ptr

    return 0;
}

Pointer Declarations

cpp
int*    ip;     // Pointer to int
double* dp;     // Pointer to double
char*   cp;     // Pointer to char

// Declare multiple pointers on one line — '*' binds to the NAME, not the type
int *p1, *p2;   // Both pointers to int
int* p3, p4;    // p3 is pointer, p4 is a plain int! (common mistake)

// Pointer to const (the data cannot be changed through this pointer)
const int* cptr = &x;
// *cptr = 10;  // ERROR

// Const pointer (the pointer itself cannot be reassigned)
int* const pconst = &x;
pconst = &y;    // ERROR
*pconst = 10;   // OK — data can be changed

// Const pointer to const (neither can change)
const int* const cpconst = &x;

nullptr

Always initialise pointers — use nullptr (C++11) when there's no valid target:

cpp
#include <iostream>
using namespace std;

int main() {
    int* p = nullptr;   // Does not point to anything

    // Always check before dereferencing
    if (p != nullptr) {
        cout << *p << "\n";
    } else {
        cout << "Pointer is null\n";
    }

    // nullptr converts to false in boolean context
    if (!p) {
        cout << "Null pointer\n";
    }

    // Assigning after declaration
    int x = 99;
    p = &x;
    cout << *p << "\n";  // 99

    return 0;
}

Warning: Dereferencing a null or invalid pointer causes undefined behaviour — typically a segmentation fault crash. Always initialise pointers to nullptr and check before use.


Pointers and Arrays

Arrays and pointers are closely related — an array name decays to a pointer to its first element:

cpp
#include <iostream>
using namespace std;

int main() {
    int arr[] = {10, 20, 30, 40, 50};

    int* p = arr;     // Points to arr[0]

    cout << *p        << "\n";  // 10  — arr[0]
    cout << *(p + 1)  << "\n";  // 20  — arr[1]
    cout << *(p + 4)  << "\n";  // 50  — arr[4]
    cout << p[2]      << "\n";  // 30  — arr[2] (same as *(p+2))

    // Pointer arithmetic — moves by sizeof(type) bytes
    for (int* q = arr; q < arr + 5; q++) {
        cout << *q << " ";
    }
    cout << "\n";  // 10 20 30 40 50

    // Difference between pointers
    int* first = &arr[0];
    int* last  = &arr[4];
    cout << "Distance: " << (last - first) << "\n";  // 4

    return 0;
}

References

A reference is an alias for an existing variable — once bound, it cannot be rebound:

cpp
#include <iostream>
using namespace std;

int main() {
    int x = 10;
    int& ref = x;    // ref is an alias for x

    cout << ref << "\n";   // 10
    ref = 99;              // Modifies x through ref
    cout << x   << "\n";   // 99

    // ref and x are the same memory location
    cout << (&ref == &x) << "\n";  // 1 (true)

    // References must be initialised at declaration
    // int& bad;  // ERROR — unbound reference

    // Const reference — can bind to temporaries and literals
    const int& cref = 42;   // Extends lifetime of the temporary
    cout << cref << "\n";   // 42

    return 0;
}

Pointers vs References

FeaturePointerReference
Null allowedYes (nullptr)No — must bind to a valid object
RebindableYes — can point to different objectsNo — always refers to the original
Syntax to dereference*ptrTransparent (just use ref)
ArithmeticYes — ptr + nNo
Use for optional parametersYesNo
Use for pass-by-referenceBoth workReferences are cleaner
Use in data structuresYesLimited (cannot be stored in containers)

Pointer to Pointer

cpp
#include <iostream>
using namespace std;

int main() {
    int x = 42;
    int* p  = &x;     // Points to x
    int** pp = &p;    // Points to p (pointer to pointer)

    cout << x    << "\n";   // 42
    cout << *p   << "\n";   // 42
    cout << **pp << "\n";   // 42

    **pp = 100;   // Modify x through two levels of indirection
    cout << x    << "\n";   // 100

    return 0;
}

void* Pointer

A void* holds a pointer to any type — used in generic C APIs:

cpp
#include <iostream>
using namespace std;

void printBytes(void* data, size_t count) {
    unsigned char* bytes = static_cast<unsigned char*>(data);
    for (size_t i = 0; i < count; i++) {
        cout << hex << (int)bytes[i] << " ";
    }
    cout << dec << "\n";
}

int main() {
    int n = 0xDEADBEEF;
    printBytes(&n, sizeof(n));   // Shows the bytes in memory order

    return 0;
}

Function Pointers

cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// A regular function
int square(int x) { return x * x; }
int cube(int x)   { return x * x * x; }

// Function taking a function pointer
void applyAndPrint(int x, int (*transform)(int)) {
    cout << "Result: " << transform(x) << "\n";
}

int main() {
    // Declare a function pointer
    int (*fp)(int) = square;
    cout << fp(5) << "\n";    // 25

    fp = cube;
    cout << fp(5) << "\n";    // 125

    applyAndPrint(4, square);  // Result: 16
    applyAndPrint(4, cube);    // Result: 64

    // Array of function pointers
    int (*ops[])(int) = { square, cube };
    for (auto op : ops) cout << op(3) << " ";
    cout << "\n";   // 9 27

    return 0;
}

In modern C++, prefer std::function or lambdas over raw function pointers — they are cleaner and more flexible.


Practical Example: Linked List with Pointers

cpp
#include <iostream>
using namespace std;

struct Node {
    int   data;
    Node* next;

    Node(int val) : data(val), next(nullptr) {}
};

class LinkedList {
    Node* head;

public:
    LinkedList() : head(nullptr) {}

    // Insert at front
    void push(int val) {
        Node* node = new Node(val);
        node->next = head;
        head = node;
    }

    // Remove and return front
    int pop() {
        if (!head) throw runtime_error("Empty list");
        int val = head->data;
        Node* temp = head;
        head = head->next;
        delete temp;
        return val;
    }

    void print() const {
        Node* curr = head;
        while (curr) {
            cout << curr->data << " -> ";
            curr = curr->next;
        }
        cout << "null\n";
    }

    bool contains(int val) const {
        Node* curr = head;
        while (curr) {
            if (curr->data == val) return true;
            curr = curr->next;
        }
        return false;
    }

    // Destructor — free all nodes
    ~LinkedList() {
        Node* curr = head;
        while (curr) {
            Node* next = curr->next;
            delete curr;
            curr = next;
        }
    }
};

int main() {
    LinkedList list;
    list.push(1);
    list.push(2);
    list.push(3);
    list.print();           // 3 -> 2 -> 1 -> null

    cout << list.pop() << "\n";   // 3
    list.print();                 // 2 -> 1 -> null

    cout << list.contains(2) << "\n";  // 1 (true)
    cout << list.contains(5) << "\n";  // 0 (false)

    return 0;
}

Summary

  • A pointer stores a memory address; dereference with *, take address with &
  • Always initialise pointers — use nullptr when there's no valid target
  • Pointer arithmetic steps by sizeof(type) — ptr + 1 moves to the next element, not the next byte
  • A reference is a permanent alias — it cannot be null, cannot be rebound, and has cleaner syntax
  • Use references for function parameters when you want to avoid copying but not allow null
  • Use pointers when you need nullability or rebindability, or when working with arrays
  • const int* — pointer to const data; int* const — const pointer to mutable data; const int* const — both
  • Function pointers store the address of a function; in modern C++ prefer std::function or lambdas

Next, we'll learn about dynamic memory — new, delete, heap allocation, and how memory leaks happen.