Skip to content
Shafin Chowdhury
Go back

Pointers in C++: Memory Addresses, Dereferencing, and Heap Basics

Software Developer & Computer Science Student

Published:
6 min read
Edit page
Table of Contents10 topics

Pointers are often considered the defining milestone in a C++ programmer’s journey. They give you direct access to computer memory, enabling high-performance data structures, dynamic memory management, low-level hardware control, and seamless interaction with operating system APIs.

At the same time, misusing pointers is the leading cause of crashes, segmentation faults, and security vulnerabilities.

In this guide, we demystify pointers completely—breaking down how memory addresses work, how to dereference safely, how pointer arithmetic operates, and how modern C++ handles dynamic heap allocation.


What You’ll Learn

  • How computer memory is structured as a linear address space
  • What a pointer is and how it differs from a regular variable
  • The address-of operator (&) vs. the dereference operator (*)
  • How to declare, initialize, and reassign pointers safely
  • The importance of nullptr over legacy NULL or 0
  • The relationship between pointers, array decay, and pointer arithmetic
  • Pointers to pointers (T**) and multi-level indirection
  • Dynamic heap allocation: new, delete, and avoiding memory leaks
  • Common pitfalls: dangling pointers, memory leaks, and wild pointers
  • Why modern C++ embraces RAII and smart pointers

Understanding Computer Memory

Before writing a single line of pointer code, visualize computer memory (RAM).

Computer memory is essentially a vast, linear array of numbered storage cells called bytes. Each individual byte has a unique numerical identifier called its memory address:

Memory Address:  0x1000   0x1001   0x1002   0x1003   0x1004   0x1005
Stored Byte:     [ 0x2A ] [ 0x00 ] [ 0x00 ] [ 0x00 ] [ 0x48 ] [ 0x69 ]
                 |-------- 4-byte integer --------|  |-- 2 chars ---|

When you declare a regular variable:

int age = 42;

The compiler reserves 4 bytes on the runtime stack (for example, at address 0x7ffd5e3a89bc) and stores the binary representation of 42 in those bytes. The identifier age is simply a convenient name human programmers use to refer to that memory address.


The Address-Of Operator (&)

You can inspect the exact memory address where a variable lives using the address-of operator (&):

#include <iostream>

using namespace std;

int main() {
    int score = 95;

    cout << "Value of score:   " << score << '\n';
    cout << "Address of score: " << &score << '\n';

    return 0;
}

Output (Sample)

Value of score:   95
Address of score: 0x7ffee4b6389c

The hexadecimal number 0x7ffee4b6389c is the exact starting byte in RAM where score is stored.


What Is a Pointer?

A pointer is simply a variable whose value is the memory address of another variable.

Just as an int variable stores an integer value like 42, an int* pointer variable stores a memory address like 0x7ffee4b6389c.

Variable:  score (int)                  Pointer:  ptr (int*)
Address:   0x7ffee4b6389c               Address:  0x7ffee4b638a0
Value:     95                           Value:    0x7ffee4b6389c
                                                  |
                                                  +---> Points to 'score'

Declaring and Initializing Pointers

To declare a pointer, place an asterisk (*) between the data type and the pointer variable name:

int* ptr = nullptr; // A pointer that can store the address of an int

[!TIP] In modern C++, prefer placing the asterisk next to the type (int* ptr;) rather than the name (int *ptr;). This reinforces the mental model that int* is a distinct type: “pointer to int”.

Initializing a Pointer

You initialize a pointer by assigning it the address of a compatible variable:

int count = 10;
int* pCount = &count; // pCount now holds the memory address of count

Dereferencing a Pointer (*)

Once a pointer holds an address, how do you read or write the actual value sitting at that address?

You use the dereference operator (*):

#include <iostream>

using namespace std;

int main() {
    int target = 50;
    int* ptr = &target;

    cout << "Address held by ptr: " << ptr << '\n';
    cout << "Value pointed to:     " << *ptr << '\n'; // Dereference (reads 50)

    // Modifying target THROUGH the pointer:
    *ptr = 100;

    cout << "Updated value of target: " << target << '\n'; // Now 100!
    return 0;
}

Output

Address held by ptr: 0x7ffee4b6389c
Value pointed to:     50
Updated value of target: 100

Crucial Distinction: * in Declaration vs. * in Expression

  • In a type declaration, * means “pointer type”:
    int* p; // 'p' is a pointer to an int
  • In an executable expression, * means “dereference / follow the address”:
    *p = 25; // Store 25 at the address p points to

Null Pointers and nullptr

An uninitialized pointer is called a wild pointer. It contains a random memory address (garbage value). Dereferencing an uninitialized pointer attempts to read or write random memory, triggering an immediate crash or silent memory corruption:

int* badPtr; // DANGEROUS! Points to random memory
// *badPtr = 10; // CRASH: Segmentation fault

To prevent this, always initialize pointers that do not yet point to valid memory with nullptr (introduced in C++11):

int* safePtr = nullptr; // Explicitly points to nothing (address 0)

Always Check Before Dereferencing

if (safePtr != nullptr) {
    cout << *safePtr << '\n';
} else {
    cout << "Pointer is null; cannot dereference.\n";
}

[!NOTE] In modern C++, always use nullptr instead of legacy NULL or 0. nullptr is strongly typed (std::nullptr_t), eliminating ambiguous overload errors when functions accept both integers and pointers.


Pointers and Arrays: Array Decay and Pointer Arithmetic

In C++, there is a deep relationship between raw arrays and pointers. The name of an array acts as a constant pointer to its first element (arr≡&arr[0]arr \equiv \&arr[0]):

int numbers[3] = {10, 20, 30};
int* p = numbers; // Array decays to &numbers[0]

Pointer Arithmetic

When you add or subtract an integer from a pointer, the compiler does not advance the address by that many bytes—it advances the address by multiples of the underlying data type’s size:

New Address=Current Address+(n×sizeof(T))\text{New Address} = \text{Current Address} + \left( n \times \text{sizeof}(T) \right)

#include <iostream>

using namespace std;

int main() {
    int arr[3] = {100, 200, 300};
    int* p = arr; // Points to arr[0]

    cout << "Value at p:     " << *p << " (Address: " << p << ")\n";

    p++; // Advances by sizeof(int) = 4 bytes!
    cout << "Value at p + 1: " << *p << " (Address: " << p << ")\n";

    p++; // Advances another 4 bytes to arr[2]
    cout << "Value at p + 2: " << *p << " (Address: " << p << ")\n";

    return 0;
}

Array indexing arr[i] is literally translated by the compiler as *(arr + i).


Pointers and Functions

Passing a pointer into a function allows the function to modify the caller’s variable directly, bypassing pass-by-value copying:

#include <iostream>

using namespace std;

void swapNumbers(int* a, int* b) {
    if (a == nullptr || b == nullptr) return;

    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 10, y = 20;

    cout << "Before swap: x=" << x << ", y=" << y << '\n';
    swapNumbers(&x, &y);
    cout << "After swap:  x=" << x << ", y=" << y << '\n';

    return 0;
}

Pointers to Pointers (Multi-Level Indirection)

Because a pointer is itself a variable stored in memory, it has its own memory address. A pointer that stores the address of another pointer is called a pointer to a pointer (T**):

int val = 42;
int* p = &val;    // p points to val
int** pp = &p;    // pp points to p

cout << **pp; // Dereferences twice to reach 42!
pp (int**)          p (int*)            val (int)
[ 0x1000 ] ------>  [ 0x2000 ] ------>  [ 42 ]
Address: 0x3000     Address: 0x1000     Address: 0x2000

Dynamic Memory Allocation: Stack vs. Heap

In C++, memory is split into two primary runtime regions:

  1. The Stack: Automatically managed by the compiler. Local variables are pushed on function entry and popped on function exit. It is extremely fast, but has a fixed size (typically a few megabytes).
  2. The Heap (Free Store): A large pool of memory managed manually at runtime. You allocate memory on the heap when:
    • You don’t know how much memory you will need until runtime.
    • You need data to outlive the function that created it.

Allocating and Freeing with new and delete

#include <iostream>

using namespace std;

int main() {
    // 1. Allocate a single integer on the heap:
    int* heapInt = new int(42);

    cout << "Heap value: " << *heapInt << '\n';

    // 2. Free the allocated heap memory:
    delete heapInt;

    // 3. Reset pointer to nullptr so it doesn't become a dangling pointer:
    heapInt = nullptr;

    // Allocating dynamic arrays:
    int size = 5;
    int* dynamicArr = new int[size]{1, 2, 3, 4, 5};

    // Free dynamic arrays with delete[]:
    delete[] dynamicArr;
    dynamicArr = nullptr;

    return 0;
}

The Three Lethal Pointer Bugs

1. Memory Leaks

Occur when heap memory is allocated with new, but the pointer pointing to it is overwritten or goes out of scope without calling delete:

void badFunction() {
    int* data = new int[10000];
    // Forgot to call delete[] data;
} // 'data' pointer variable is destroyed, but the 40,000 bytes remain allocated forever!

2. Dangling Pointers

Occur when memory is freed using delete, but the pointer continues to hold the deallocated address:

int* p = new int(10);
delete p; // Memory is released to the OS!
// BUG: p still points to the old address!
// *p = 20; // Undefined behavior: modifying memory you no longer own!

3. Double Free

Calling delete twice on the exact same heap address corrupts the internal memory allocator’s metadata and crashes the program immediately.


Modern C++ Perspective: RAII and Smart Pointers

In modern C++ (C++11 and newer), manual management with raw new and delete is strongly discouraged in general application code.

Instead, modern C++ relies on RAII (Resource Acquisition Is Initialization) and smart pointers (std::unique_ptr and std::shared_ptr from <memory>):

#include <memory>

using namespace std;

void modernStyle() {
    // Automatically allocated on the heap and GUARANTEED to be freed
    // when myPtr leaves scope—even if an exception is thrown!
    auto myPtr = make_unique<int>(100);
} // Automatically deleted here! No memory leaks possible.

Summary

  • Computer memory is an indexed sequence of bytes, each with a unique hexadecimal address.
  • The address-of operator (&) retrieves the memory address of an lvalue.
  • A pointer (T*) is a variable that holds the memory address of another object.
  • The dereference operator (*) reads or writes the value located at the stored address.
  • Always initialize unused pointers to nullptr.
  • Pointer arithmetic advances addresses in units of sizeof(T) bytes.
  • Dynamic memory allocated with new must always be paired with delete.
  • Modern C++ uses smart pointers to automate memory lifecycle management.

Continue Learning C++


Edit page
Share this post:

Related Articles

Continue exploring related topics and foundational concepts.


Previous Post
References in C++: Aliases, Memory Mechanics, and Pointers Compared
Next Post
Strings in C++: std::string vs C-Style Strings and Practical Operations