Memory & Pointers

Pointers and References in C++

DIFFICULTY: Intermediate READ TIME: 15 mins

Hey! Welcome back. Today, we’re unlocking one of the most famous (and sometimes feared) features of C++: Pointers and References.

In previous chapters, you read that C++ gives you direct control over hardware and memory. Pointers and references are the actual tools you use to wield that control. They allow you to access memory addresses directly, pass data around without slow copy operations, and build complex dynamic structures.

Don’t worry if you’ve heard pointers are confusing—we’re going to break them down step-by-step with simple analogies!


Pointers in C++

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

Think of a normal variable (like int x = 10;) like a house. The house contains some furniture (the value 10). A pointer is like a piece of paper with the street address of that house written on it (e.g., 123 Main Street). If you look at the pointer, you don’t see the furniture; you just see the address. But if you follow that address, you find the house and the furniture inside.


Declaring and Initializing Pointers

To declare a pointer, you place an asterisk * between the data type and the pointer’s name.

Syntax:

data_type* name_of_pointer;

The data type must match the type of variable whose address you want to store.

To store the address of a variable in your pointer, you use the Address-Of Operator (&).

Example:

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

Understanding the & and * Operators

  • & (Address-Of Operator): Placed before a variable, it returns its memory address.
  • * (Dereference Operator): Placed before a pointer, it tells C++ to jump to the address stored in the pointer and retrieve/modify the value stored there.

Here’s how they link together in memory:

graph LR
    ptr[Pointer variable: ptr <br/> Value: 0x61fea4] -->|Points to| s[String Variable: s <br/> Address: 0x61fea4 <br/> Value: 'Hey, striverA2Z!']
    deref[*ptr] -.->|Dereferences to yield| val[Value: 'Hey, striverA2Z!']

Let’s see this in code:

Example:

#include <iostream>
#include <string>

int main() {
    std::string s = "Hey, striverA2Z!";
    std::string* ptr = &s; // ptr points to s
    
    std::cout << "Value of s: " << s << std::endl;
    std::cout << "Address of s (&s): " << &s << std::endl;
    std::cout << "Value stored in ptr (address): " << ptr << std::endl;
    std::cout << "Value pointed to by ptr (*ptr): " << *ptr << std::endl;
    
    // We can also modify the variable through the pointer!
    *ptr = "Hello C++ Masters!";
    std::cout << "Modified value of s: " << s << std::endl;
    
    return 0;
}

Output:

Value of s: Hey, striverA2Z!
Address of s (&s): 0x61fea4
Value stored in ptr (address): 0x61fea4
Value pointed to by ptr (*ptr): Hey, striverA2Z!
Modified value of s: Hello C++ Masters!

nullptr (Null Pointer)

What if you declare a pointer but don’t want it pointing to any random address in memory yet? You should initialize it to nullptr.

A null pointer explicitly points to nothing (0), ensuring that if you accidentally try to dereference it, your program crashes cleanly instead of silently corrupting random memory spots.

Example:

#include <iostream>

int main() {
    int* ptr = nullptr;
    
    if (ptr == nullptr) {
        std::cout << "Pointer is currently pointing to nothing." << std::endl;
    }
    
    return 0;
}

Output:

Pointer is currently pointing to nothing.

Pointers and Arrays

In C++, an array’s name is actually a constant pointer to its first element!

Example:

#include <iostream>

int main() {
    int arr[3] = {10, 20, 30};
    
    std::cout << "Array name value (base address): " << arr << std::endl;
    std::cout << "Address of first element (&arr[0]): " << &arr[0] << std::endl;
    std::cout << "Value at base address (*arr): " << *arr << std::endl;
    
    return 0;
}

Output:

Array name value (base address): 0x61fe90
Address of first element (&arr[0]): 0x61fe90
Value at base address (*arr): 10

Pointer Arithmetic

You can perform arithmetic operations (like addition and subtraction) on pointers.

When you increment a pointer (ptr++), C++ doesn’t just add 1 to the memory address. Instead, it increments the address by the size of the data type it points to.

  • If it’s an int* (4 bytes), ptr + 1 increases the address by 4.
  • If it’s a double* (8 bytes), ptr + 1 increases the address by 8.
graph TD
    subgraph Array in Memory: arr = {10, 20, 30, 40, 50}
        direction LR
        m0[Val: 10 <br/> Addr: 0x100]
        m1[Val: 20 <br/> Addr: 0x104]
        m2[Val: 30 <br/> Addr: 0x108]
        m3[Val: 40 <br/> Addr: 0x10C]
        m4[Val: 50 <br/> Addr: 0x110]
        
        m0 --- m1 --- m2 --- m3 --- m4
    end
    
    ptr_0[ptr] -->|Points to index 0| m0
    ptr_1[ptr + 1] -->|Points to index 1| m1
    ptr_2[ptr + 2] -->|Points to index 2| m2

Let’s see pointer arithmetic in action:

Example:

#include <iostream>

int main() {
    int arr[3] = {100, 200, 300};
    int* ptr = arr; // Points to arr[0]
    
    std::cout << "Address: " << ptr << ", Value: " << *ptr << std::endl;
    
    ptr++; // Move to next int (adds 4 bytes to address)
    std::cout << "Address: " << ptr << ", Value: " << *ptr << std::endl;
    
    ptr++; // Move to next int
    std::cout << "Address: " << ptr << ", Value: " << *ptr << std::endl;
    
    return 0;
}

Output:

Address: 0x61fe90, Value: 100
Address: 0x61fe94, Value: 200
Address: 0x61fe98, Value: 300

Pointer to Pointers (Double Pointers)

A pointer is a variable, so it also has its own address in memory. This means you can create a pointer that stores the address of another pointer! This is called a double pointer or pointer-to-pointer, declared with **.

Think of it like a treasure hunt:

  • You have a map (ptr2) that points to a chest.
  • Inside that chest, you find another map (ptr1) that points to the actual treasure (a).
graph LR
    ptr2[Pointer to Pointer: ptr2 <br/> Value: 0x61feb8 <br/> Addr: 0x61febc] -->|Points to| ptr1[Pointer: ptr1 <br/> Value: 0x61feb4 <br/> Addr: 0x61feb8]
    ptr1 -->|Points to| a[Variable: a <br/> Value: 10 <br/> Addr: 0x61feb4]

Let’s see this in code:

Example:

#include <iostream>

int main() {
    int a = 10;
    int* ptr1 = &a;    // Single pointer
    int** ptr2 = &ptr1; // Double pointer (pointer to pointer)
    
    std::cout << "Value of a: " << a << std::endl;
    std::cout << "Value using *ptr1: " << *ptr1 << std::endl;
    std::cout << "Value using **ptr2: " << **ptr2 << std::endl;
    std::cout << "Address of ptr1: " << &ptr1 << std::endl;
    std::cout << "Value stored in ptr2: " << ptr2 << std::endl;
    
    return 0;
}

Output:

Value of a: 10
Value using *ptr1: 10
Value using **ptr2: 10
Address of ptr1: 0x61feb8
Value stored in ptr2: 0x61feb8

Dynamic Memory Allocation

Usually, variables are allocated on the Stack, which is managed automatically. However, stack variables have a fixed size. If you want to allocate memory dynamically at runtime (e.g., creating an array whose size is decided by user input), you allocate it on the Heap using new.

[!CAUTION] Heap memory is not managed automatically. When you are done using it, you must free it using delete (or delete[] for arrays). If you forget, your program will hold onto that memory until it exits, causing a Memory Leak.

Syntax:

int* ptr = new int; // Allocates one int on the Heap
delete ptr;         // Deallocates it

int* arr = new int[size]; // Allocates an array on the Heap
delete[] arr;             // Deallocates the array

Let’s see an example allocating a dynamic array:

Example:

#include <iostream>

int main() {
    int size;
    std::cout << "Enter array size: ";
    std::cin >> size;
    
    // Allocate array on the Heap
    int* dynamicArr = new int[size];
    
    for (int i = 0; i < size; ++i) {
        dynamicArr[i] = (i + 1) * 10;
    }
    
    std::cout << "Array values: ";
    for (int i = 0; i < size; ++i) {
        std::cout << dynamicArr[i] << " ";
    }
    std::cout << std::endl;
    
    // Free the memory!
    delete[] dynamicArr;
    
    return 0;
}

Output:

Enter array size: 3
Array values: 10 20 30 

[!TIP] You can pass std::nothrow to the new operator (e.g., int* ptr = new(std::nothrow) int;). This tells C++ that if the system runs out of memory, instead of crashing the program with an exception, it should simply return nullptr.


References in C++

A reference is simply an alias (another name) for an existing variable.

Think of it like a nickname. If your friend’s name is Robert and you call him “Bob”, any action you take on “Bob” (like giving him a high-five) happens to Robert. They are not two different people; they are one person with two names.

To declare a reference, we use the & operator during declaration.

Syntax:

int a = 10;
int& ref = a; // ref is now a nickname for a

Let’s see how references work in code:

Example:

#include <iostream>

int main() {
    int a = 10;
    int& ref = a; // ref is a reference to a
    
    std::cout << "Value of a: " << a << std::endl;
    std::cout << "Value of ref: " << ref << std::endl;
    
    // Modifying ref changes a!
    ref = 50;
    std::cout << "Value of a after modifying ref: " << a << std::endl;
    
    return 0;
}

Output:

Value of a: 10
Value of ref: 10
Value of a after modifying ref: 50

[!NOTE] You can prevent a reference from modifying the original variable by declaring it as a constant reference: const int& ref = a;.


Memory Efficient Code with References

References are incredibly useful when passing arguments to functions. Instead of copying large objects (like strings or vectors), which consumes time and memory, we pass them by reference.

Example:

#include <iostream>
#include <string>

// Passing by const reference avoids copying the large string, but protects it from modification
void printMessage(const std::string& msg) {
    std::cout << "Message: " << msg << std::endl;
}

int main() {
    std::string text = "This is a super long string in memory...";
    printMessage(text); // Passed efficiently without cloning
    return 0;
}

Output:

Message: This is a super long string in memory...

Pointers vs. References

Although pointers and references are both used to access memory addresses, they have key differences:

FeaturePointersReferences
DefinitionA variable storing a memory address.An alias/nickname for a variable.
Syntaxint* ptr = &x;int& ref = x;
InitializationCan be initialized later or set to nullptr.Must be initialized at declaration.
ReassignmentCan point to different variables over time.Cannot be reassigned to refer to another variable.
Null AbilityCan be nullptr.Cannot be null (must always refer to a valid object).
Memory AddressHas its own address and occupies memory.Shares the same address and memory spot as the target.

Level Up: Extra Practice & Interview Questions

Q1. Why is Dynamic Memory Allocation necessary?

Dynamic memory allocation is essential when you don’t know how much memory you’ll need until the program is actually running. For example, if you’re writing code to load user profile entries, you can’t hardcode an array of size 100 because a user might have 5 entries or 10,000 entries. Dynamic allocation allows you to claim exactly the amount of memory you need at runtime.


Q2. Given an array int arr[] = \{10, 20, 30, 40\};, use pointer arithmetic to print the second and third elements of the array.

We can point a pointer to the array, and add offsets to it to read indices 1 and 2.

Code:

#include <iostream>

int main() {
    int arr[] = {10, 20, 30, 40};
    int* ptr = arr; // points to arr[0]
    
    // ptr + 1 points to arr[1], ptr + 2 points to arr[2]
    std::cout << "Second element: " << *(ptr + 1) << std::endl;
    std::cout << "Third element: " << *(ptr + 2) << std::endl;
    
    return 0;
}

Output:

Second element: 20
Third element: 30

Q3. Write a program that declares an integer variable x with value 5, creates a pointer ptr pointing to x, and prints the memory address and value it is pointing at.

This verifies the link between pointer storage and variable lookup.

Code:

#include <iostream>

int main() {
    int x = 5;
    int* ptr = &x;
    
    std::cout << "Address stored in ptr: " << ptr << std::endl;
    std::cout << "Value ptr is pointing to: " << *ptr << std::endl;
    
    return 0;
}

Output:

Address stored in ptr: 0x61fe88
Value ptr is pointing to: 5

quiz Test Your Understanding

What does the * operator do when applied to an existing pointer variable?