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