Pointers and Address of Operators
Pointers, References and Memory Layout

3.1 Pointers and Address of Operators

I will explain the most feared concept in C++: pointers. In many high level languages, pointers are hidden behind objects and garbage collection. In C++, they are raw, exposed numbers representing index coordinates of your RAM. Once you realize a pointer is just an integer that stores a memory address, the fear disappears.

Let's look at how memory addresses work, how to create pointers, and how to read the memory they point to.

Memory Addresses: The Post Office Boxes of RAM

Think of your computer's RAM as a very long street with billions of houses. Each house represents one byte of memory, and each house has a unique physical address number (typically written in hexadecimal, like 0x7ffee3c8a1b0).

When we declare a variable, the operating system assigns it to one of these houses. A pointer is simply a variable whose value is the address of another house.

The Address of Operator (`&`) and Pointers

To find the memory address of a variable, we use the address of operator &. To declare a variable that can store this address, we use the pointer declaration syntax (appending * to the data type):

#include <iostream>

int main() {
    int count{42};
    
    // Declare an integer pointer and assign it the address of count
    int* ptr{&count};

    std::cout << "Value of count: " << count << '\n';
    std::cout << "Address of count (&count): " << &count << '\n';
    std::cout << "Value stored in ptr (address): " << ptr << '\n';
    return 0;
}
Declaring pointers and storing memory addresses.

The Dereference Operator (`*`)

Once you have a pointer storing an address, you need a way to go inside that house and inspect or modify its contents. We do this using the dereference operator, which is also the symbol (yes, C++ uses for multiplication, pointer declarations, and dereferencing; context is everything).

int score{100};
int* scorePtr{&score};

// Read the value at the address scorePtr points to
std::cout << *scorePtr << '\n'; // Prints 100

// Modify the value at that address directly!
*scorePtr = 250;
std::cout << score << '\n';    // Prints 250! The original variable has changed.
Reading and writing memory through dereferencing.

Null Pointers (The Blank Note Card Hazard)

A pointer that is declared but not initialized will contain garbage memory. If you try to dereference it, your program will access random, protected RAM, triggering a crash.

To avoid this, we initialize empty pointers to nullptr (introduced in C++11). A nullptr represents address 0x0, which is guaranteed by the operating system to be unreadable and unwritable.

* The Treasure Map Metaphor: Think of a pointer as a treasure map. The coordinates on the map show you where to go. Dereferencing is traveling to those coordinates and digging up the treasure. If your pointer is set to nullptr, it is like looking at a blank map. If you try to travel to a blank coordinate to dig, you run into a brick wall at warp speed, and the operating system arrests your program. This crash is called a segmentation fault (or access violation).

int* ptr{nullptr}; // Guaranteed to point to nothing

// Always check if a pointer is safe before dereferencing!
if (ptr != nullptr) {
    std::cout << *ptr << '\n';
} else {
    std::cout << "Pointer is empty; skipping dereference to prevent crash!\n";
}
Safe pointer checks before dereferencing.

Pointer Arithmetic (Stretchy Address Increments)

CPUs allow you to add integers to pointers, which is called pointer arithmetic. However, adding 1 to a pointer does not add one byte to its address value! Instead, C++ offsets the address by the size of the underlying data type.

If you have an integer pointer (pointing to a 4-byte int) and add 1, the memory address shifts forward by exactly 4 bytes! This allows you to step through array data blocks with zero latency:

int values[3]{10, 20, 30};
int* p{values}; // Points to values[0]

std::cout << *p << '\n';       // Prints 10
std::cout << *(p + 1) << '\n'; // Prints 20! Address shifts by 4 bytes (sizeof(int))
std::cout << *(p + 2) << '\n'; // Prints 30! Address shifts by 8 bytes
Stepping through memory using pointer arithmetic.

Void Pointers (Generic Addresses)

Sometimes you need to store a memory address without knowing the data type of the value stored there. C++ provides void* for this purpose.

A void pointer can hold the address of any variable type, but because the compiler does not know the size of the data type, you cannot dereference a void pointer directly. You must explicitly cast it back to its specific type first:

int data{500};
void* genericPtr{&data}; // Valid! Holds the address of an integer

// std::cout << *genericPtr; // COMPILER ERROR: Cannot dereference void pointer directly!

// Explicitly cast to integer pointer to dereference
int* intPtr{static_cast<int*>(genericPtr)};
std::cout << *intPtr << '\n'; // Prints 500
Storing and casting generic void pointers.
Finished reading this lesson?