Chapter 06

Pointers

Understanding memory addresses and indirect access to data

What You Will Learn

  • What pointers are and why they're useful
  • How to declare, initialize, and use pointers
  • Pointer arithmetic and array relationships
  • Working with null pointers safely
  • Void pointers and type casting
  • Const correctness with pointers
  • Function pointers for callbacks
  • Differences between pointers and references
  • Introduction to smart pointers

What Are Pointers?

A pointer is a variable that stores the memory address of another variable.

int x = 42;       // x is stored at some memory address
int* ptr = &x;   // ptr stores the address of x

// Visualization:
// Variable   Value    Address (example)
// x          42       0x7ffd5e8c
// ptr        0x7ffd5e8c  0x7ffd5e90

Why Use Pointers?

  • Dynamic memory - Allocate memory at runtime
  • Efficient passing - Pass large objects without copying
  • Data structures - Build linked lists, trees, graphs
  • Polymorphism - Enable runtime polymorphism in OOP
  • Hardware access - Work with memory-mapped I/O
  • Callbacks - Pass functions as arguments

Declaring Pointers

// Syntax: type* pointer_name;
int* intPtr;           // Pointer to int
double* doublePtr;     // Pointer to double
char* charPtr;         // Pointer to char

// The * can be placed differently (all equivalent)
int *ptr1;   // Style 1
int* ptr2;   // Style 2 (preferred in modern C++)
int * ptr3;  // Style 3

// Multiple declarations (be careful!)
int* a, b;   // a is pointer, b is int!
int *a, *b;  // Both are pointers

Initializing Pointers

int x = 42;

// Get address with & (address-of operator)
int* ptr = &x;

// Initialize to null
int* nullPtr = nullptr;  // C++11 and later
int* nullPtr2 = NULL;    // C-style (avoid)
int* nullPtr3 = 0;       // Also works (avoid)

// Never leave uninitialized!
int* dangerous;  // Points to random memory - DANGEROUS!

Dereferencing

Dereferencing accesses the value at the address a pointer holds.

int x = 42;
int* ptr = &x;

// Dereference with * operator
std::cout << *ptr;  // 42 (value of x)

// Modify through pointer
*ptr = 100;
std::cout << x;     // 100 (x was modified!)

// Read and write
int y = *ptr;       // y = 100
*ptr = *ptr + 1;    // x = 101

Address vs Value

int x = 42;
int* ptr = &x;

std::cout << ptr;   // 0x7ffd5e8c (address)
std::cout << *ptr;  // 42 (value at address)
std::cout << &ptr;  // 0x7ffd5e90 (address of pointer itself)

Null Pointers

A null pointer doesn't point to any valid memory location.

// nullptr (C++11) - the preferred way
int* ptr = nullptr;

// Checking for null
if (ptr == nullptr) {
    std::cout << "Pointer is null\n";
}

// Implicit conversion to bool
if (ptr) {
    std::cout << "Pointer is valid\n";
} else {
    std::cout << "Pointer is null\n";
}

// DANGER: Dereferencing null causes crash!
// *ptr = 42;  // Segmentation fault!

Safe Pointer Access

void processData(int* data) {
    // Always check before dereferencing
    if (data == nullptr) {
        std::cout << "Error: null pointer\n";
        return;
    }
    
    // Safe to use
    std::cout << "Value: " << *data << "\n";
}

// Optional: Use references when null isn't allowed
void processDataSafe(int& data) {
    // References can't be null
    std::cout << "Value: " << data << "\n";
}

Pointer Arithmetic

Pointers can be incremented/decremented to navigate memory.

int arr[] = {10, 20, 30, 40, 50};
int* ptr = arr;

std::cout << *ptr;      // 10
std::cout << *(ptr+1);  // 20
std::cout << *(ptr+2);  // 30

// Increment moves by sizeof(type)
ptr++;                   // Now points to arr[1]
std::cout << *ptr;      // 20

ptr += 2;                // Now points to arr[3]
std::cout << *ptr;      // 40

ptr--;                   // Now points to arr[2]
std::cout << *ptr;      // 30

Pointer Subtraction

int arr[] = {10, 20, 30, 40, 50};
int* start = &arr[0];
int* end = &arr[4];

// Difference gives number of elements
ptrdiff_t diff = end - start;  // 4

// Useful for finding index
int* ptr = &arr[2];
int index = ptr - arr;  // 2

Comparison

int arr[5];
int* p1 = &arr[1];
int* p2 = &arr[3];

if (p1 < p2) {
    std::cout << "p1 comes before p2\n";
}

// Useful for iteration
int* ptr = arr;
int* end = arr + 5;
while (ptr < end) {
    std::cout << *ptr++ << " ";
}

How Pointer Arithmetic Works

// Adding 1 doesn't add 1 byte, but sizeof(type) bytes
int* intPtr;
char* charPtr;
double* doublePtr;

// If all start at address 0x1000:
// intPtr + 1 = 0x1004     (4 bytes for int)
// charPtr + 1 = 0x1001    (1 byte for char)
// doublePtr + 1 = 0x1008  (8 bytes for double)

Pointers and Arrays

Array names decay to pointers in most contexts.

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

// Array name is a pointer to first element
int* ptr = arr;        // Same as: int* ptr = &arr[0];

// These are equivalent
std::cout << arr[2];   // 30
std::cout << *(arr+2); // 30
std::cout << ptr[2];   // 30
std::cout << *(ptr+2); // 30

Iterating with Pointers

int arr[] = {1, 2, 3, 4, 5};
int size = 5;

// Method 1: Index
for (int i = 0; i < size; i++) {
    std::cout << arr[i] << " ";
}

// Method 2: Pointer arithmetic
for (int* p = arr; p < arr + size; p++) {
    std::cout << *p << " ";
}

// Method 3: Pointer with index
int* ptr = arr;
for (int i = 0; i < size; i++) {
    std::cout << ptr[i] << " ";
}

Arrays as Function Parameters

// These declarations are equivalent for parameters
void func(int arr[]);     // Looks like array
void func(int arr[10]);   // Size is ignored
void func(int* arr);      // Actually a pointer

// You need to pass size separately
void printArray(int* arr, int size) {
    for (int i = 0; i < size; i++) {
        std::cout << arr[i] << " ";
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    printArray(arr, 5);
}

Differences

int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;

// sizeof difference
sizeof(arr);   // 20 (5 * 4 bytes)
sizeof(ptr);   // 8 (pointer size on 64-bit)

// Assignment
// arr = ptr;  // Error! Can't reassign array
ptr = arr;     // OK

// Address-of
&arr;    // Pointer to array: int(*)[5]
&ptr;    // Pointer to pointer: int**

Pointer to Pointer

Pointers can point to other pointers, creating multiple levels of indirection.

int x = 42;
int* ptr = &x;        // Pointer to x
int** pptr = &ptr;    // Pointer to ptr

std::cout << x;       // 42
std::cout << *ptr;    // 42
std::cout << **pptr;  // 42

// Modify through double pointer
**pptr = 100;
std::cout << x;       // 100

Use Cases

// 1. Dynamic 2D arrays
int** matrix = new int*[rows];
for (int i = 0; i < rows; i++) {
    matrix[i] = new int[cols];
}

// 2. Modifying a pointer in a function
void allocate(int** ptr) {
    *ptr = new int(42);
}

int* myPtr = nullptr;
allocate(&myPtr);
std::cout << *myPtr;  // 42

// 3. Array of strings (C-style)
const char* names[] = {"Alice", "Bob", "Charlie"};
// names is: const char**

Void Pointers

void* is a generic pointer that can point to any type.

int x = 42;
double y = 3.14;

void* ptr;
ptr = &x;   // OK
ptr = &y;   // OK

// Can't dereference directly
// std::cout << *ptr;  // Error!

// Must cast first
ptr = &x;
int* intPtr = static_cast<int*>(ptr);
std::cout << *intPtr;  // 42

Use Cases

// Generic functions (like C's qsort)
void* memcpy(void* dest, const void* src, size_t n);

// Custom allocators
void* myAlloc(size_t size) {
    return malloc(size);
}

// Type-erased callbacks
struct Callback {
    void (*func)(void* data);
    void* data;
};

// Note: Prefer templates in modern C++

Modern Alternative

In modern C++, prefer templates, std::any, or std::variant over void pointers for type-safe generics.

Const and Pointers

There are multiple ways to combine const with pointers.

Pointer to Const

Can't modify the value, but can change what it points to.

int x = 10, y = 20;
const int* ptr = &x;  // Or: int const* ptr

// *ptr = 15;   // Error! Can't modify value
ptr = &y;       // OK, can point elsewhere
std::cout << *ptr;  // 20

Const Pointer

Can modify the value, but can't change what it points to.

int x = 10, y = 20;
int* const ptr = &x;

*ptr = 15;       // OK, can modify value
// ptr = &y;     // Error! Can't point elsewhere
std::cout << *ptr;  // 15

Const Pointer to Const

Can't modify value or change what it points to.

int x = 10, y = 20;
const int* const ptr = &x;

// *ptr = 15;   // Error!
// ptr = &y;    // Error!

Reading Const Declarations

// Read right to left:
int* ptr;              // ptr is a pointer to int
const int* ptr;        // ptr is a pointer to const int
int* const ptr;        // ptr is a const pointer to int
const int* const ptr;  // ptr is a const pointer to const int

// Mnemonic: const before * = const value
//           const after * = const pointer

Function Pointers

Pointers can store the address of functions.

// Function to point to
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

// Declare function pointer
int (*operation)(int, int);

// Assign and call
operation = add;
std::cout << operation(5, 3);  // 8

operation = subtract;
std::cout << operation(5, 3);  // 2

Type Aliases

// Using (modern, preferred)
using BinaryOp = int(*)(int, int);

// Typedef (old style)
typedef int (*BinaryOp)(int, int);

BinaryOp op = add;
std::cout << op(10, 5);  // 15

Callbacks

void forEach(int arr[], int size, void (*callback)(int)) {
    for (int i = 0; i < size; i++) {
        callback(arr[i]);
    }
}

void printDouble(int x) {
    std::cout << x * 2 << " ";
}

void printSquare(int x) {
    std::cout << x * x << " ";
}

int arr[] = {1, 2, 3, 4, 5};
forEach(arr, 5, printDouble);  // 2 4 6 8 10
forEach(arr, 5, printSquare);  // 1 4 9 16 25

Array of Function Pointers

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }

// Array of function pointers
int (*operations[])(int, int) = {add, sub, mul, div};

// Call by index
std::cout << operations[0](10, 5);  // 15 (add)
std::cout << operations[2](10, 5);  // 50 (mul)

// Calculator example
int calculate(int a, int b, int opIndex) {
    return operations[opIndex](a, b);
}

References vs Pointers

Feature Pointer Reference
Syntax int* ptr int& ref
Null Can be null Cannot be null
Reassignment Can point elsewhere Bound forever
Initialization Can be uninitialized Must be initialized
Dereferencing Need * Automatic
Arithmetic Supported Not supported
Memory Has own address Alias (no memory)
// Pointer
int x = 10;
int* ptr = &x;
*ptr = 20;     // Need to dereference
ptr = nullptr; // Can be null

// Reference
int& ref = x;
ref = 30;      // No dereference needed
// Can't make ref refer to something else
// Can't be null

When to Use Each

// Use references when:
// - You need an alias that won't be null
// - Passing to functions (cleaner syntax)
void process(const std::string& str);

// Use pointers when:
// - Value might be null (optional)
// - Need to reassign to different objects
// - Working with dynamic memory
// - Need pointer arithmetic
void process(Data* optional);  // Can pass nullptr

Smart Pointers Introduction

Smart pointers manage memory automatically, preventing leaks.

The Problem with Raw Pointers

void function() {
    int* ptr = new int(42);
    
    // ... code that might throw or return early ...
    
    delete ptr;  // Might never be reached!
}

std::unique_ptr

Exclusive ownership - one owner, automatic deletion.

#include <memory>

// Create
std::unique_ptr<int> ptr = std::make_unique<int>(42);

// Use like regular pointer
std::cout << *ptr;  // 42

// Automatic cleanup when out of scope
// No delete needed!

// Transfer ownership
std::unique_ptr<int> ptr2 = std::move(ptr);
// ptr is now null, ptr2 owns the memory

std::shared_ptr

Shared ownership - multiple owners, deleted when last owner dies.

#include <memory>

std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1;  // Both own it

std::cout << ptr1.use_count();  // 2

ptr1.reset();  // ptr1 releases ownership
std::cout << ptr2.use_count();  // 1
// Memory freed when ptr2 dies

std::weak_ptr

Non-owning observer of shared_ptr.

std::shared_ptr<int> shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;

// Check if still alive
if (auto locked = weak.lock()) {
    std::cout << *locked;  // 42
}

shared.reset();  // Memory freed

if (weak.expired()) {
    std::cout << "Object is gone\n";
}

Note

Smart pointers are covered in depth in Chapter 09: Memory Management.

Common Pointer Errors

Dangling Pointer

int* ptr;
{
    int x = 42;
    ptr = &x;
}  // x destroyed
// *ptr is now dangling!

Memory Leak

void leak() {
    int* ptr = new int(42);
    // Forgot delete ptr;
}  // Memory leaked!

Double Delete

int* ptr = new int(42);
delete ptr;
delete ptr;  // Undefined behavior!

Null Dereference

int* ptr = nullptr;
*ptr = 42;  // Crash!

Array Delete Mismatch

int* arr = new int[10];
delete arr;    // Wrong! Should be delete[]

int* single = new int(42);
delete[] single;  // Wrong! Should be delete

Uninitialized Pointer

int* ptr;        // Contains garbage address
*ptr = 42;       // Undefined behavior!

Buffer Overflow

int arr[5];
int* ptr = arr;
*(ptr + 10) = 42;  // Out of bounds!

Practice Questions

  1. Write a function that swaps two integers using pointers.
  2. Implement a function to reverse an array using pointers.
  3. Create a dynamic array and resize it (similar to vector growth).
  4. Write a function to find the maximum element using pointer arithmetic.
  5. Implement a simple linked list with add and display operations.
  6. Create a function that takes a callback to transform array elements.
  7. Write a safe function that handles null pointers gracefully.
  8. Implement a 2D dynamic array using pointer to pointer.
  9. Create a program that demonstrates const correctness with pointers.
  10. Write code comparing performance of passing by value, reference, and pointer.
  11. Implement a simple memory pool allocator.
  12. Convert a pointer-based function to use smart pointers.