C++ Pointers, References, and Dereferencing: Complete Guide and Common Scenarios

Understanding pointers, references, and dereferencing is fundamental to C++ programming. This guide covers all aspects with practical scenarios.

Pointers Basics

What is a Pointer?

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

int x = 42;
int* ptr = &x;  // ptr stores the address of x

std::cout << "Value of x: " << x << std::endl;        // 42
std::cout << "Address of x: " << &x << std::endl;     // Memory address
std::cout << "Value of ptr: " << ptr << std::endl;     // Same address
std::cout << "Address of ptr: " << &ptr << std::endl;  // Address of ptr itself

Pointer Declaration and Initialization

// Declaration
int* ptr1;           // Pointer to int
int *ptr2;           // Same (spacing doesn't matter)
int * ptr3;         // Same

// Initialization
int x = 10;
int* ptr = &x;       // Points to x

// Null pointer
int* null_ptr = nullptr;  // C++11: preferred
int* null_ptr2 = NULL;    // C-style (avoid)
int* null_ptr3 = 0;       // Also works (avoid)

// Uninitialized pointer (dangerous!)
int* uninit_ptr;     // Contains garbage address - undefined behavior if used

Pointer Arithmetic

int arr[] = {10, 20, 30, 40, 50};
int* ptr = arr;      // Points to first element

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

// Increment pointer
ptr++;
std::cout << *ptr << std::endl;        // 20

// Array indexing with pointers
std::cout << ptr[0] << std::endl;      // 20
std::cout << ptr[1] << std::endl;      // 30

// Pointer difference
int* ptr2 = arr + 3;
std::cout << ptr2 - ptr << std::endl;  // 3 (number of elements between)

Pointer to Pointer

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

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

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

References Basics

What is a Reference?

A reference is an alias for an existing variable. It must be initialized and cannot be reassigned.

int x = 42;
int& ref = x;        // ref is an alias for x

std::cout << x << std::endl;           // 42
std::cout << ref << std::endl;         // 42

ref = 100;          // Modifies x
std::cout << x << std::endl;           // 100
std::cout << ref << std::endl;         // 100

Reference Declaration

int x = 10;
int& ref1 = x;       // Reference to int
int &ref2 = x;       // Same (spacing doesn't matter)
int & ref3 = x;      // Same

// Must be initialized
// int& ref;          // Error: must be initialized

// Cannot be reassigned
int y = 20;
int& ref = x;
// ref = y;          // This assigns value, doesn't rebind reference

Const References

int x = 42;
const int& cref = x;  // Const reference - cannot modify through cref

// cref = 100;       // Error: cannot modify through const reference
x = 100;             // OK: can modify original

// Temporary binding
const int& temp_ref = 42;  // OK: binds to temporary
// int& temp_ref2 = 42;    // Error: non-const cannot bind to temporary

Reference vs Pointer

int x = 42;

// Pointer
int* ptr = &x;
*ptr = 100;          // Modify through pointer
ptr = nullptr;       // Can reassign pointer

// Reference
int& ref = x;
ref = 100;           // Modify through reference (simpler syntax)
// ref = nullptr;    // Error: cannot reassign reference

// Key differences:
// 1. Reference must be initialized, pointer can be null
// 2. Reference cannot be reassigned, pointer can
// 3. Reference syntax is simpler (no * needed)
// 4. Reference cannot be null, pointer can

Dereferencing

What is Dereferencing?

Dereferencing means accessing the value at the address stored in a pointer.

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

// Dereference operator *
int value = *ptr;    // Gets value at address stored in ptr
std::cout << value << std::endl;  // 42

// Modify through pointer
*ptr = 100;
std::cout << x << std::endl;      // 100

Dereferencing Operations

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

// Basic dereference
std::cout << *ptr << std::endl;        // 10

// Increment then dereference
std::cout << *++ptr << std::endl;      // 20 (pre-increment)

// Dereference then increment
std::cout << *ptr++ << std::endl;      // 20, then ptr points to 30

// Array subscript (implicit dereference)
std::cout << ptr[0] << std::endl;      // 30
std::cout << *(ptr + 0) << std::endl;  // Same as above

// Member access through pointer
struct Point {
    int x, y;
};

Point p{10, 20};
Point* pptr = &p;

std::cout << (*pptr).x << std::endl;   // 10
std::cout << pptr->x << std::endl;      // 10 (arrow operator, preferred)

Arrow Operator (->)

struct Person {
    std::string name;
    int age;
    
    void print() {
        std::cout << name << ", " << age << std::endl;
    }
};

Person person{"Alice", 30};
Person* ptr = &person;

// Arrow operator for member access
std::cout << ptr->name << std::endl;   // "Alice"
std::cout << ptr->age << std::endl;    // 30
ptr->print();                          // Calls method

// Equivalent to
std::cout << (*ptr).name << std::endl; // Same, but arrow is preferred

Null Pointer Dereferencing

int* ptr = nullptr;

// Dangerous: dereferencing null pointer
// int value = *ptr;  // Undefined behavior - crash or corruption

// Safe: check before dereferencing
if (ptr != nullptr) {
    int value = *ptr;
}

// Or use short form
if (ptr) {
    int value = *ptr;
}

Common Scenarios

Scenario 1: Function Parameters

Pass by Value

void modify_value(int x) {
    x = 100;  // Only modifies local copy
}

int main() {
    int x = 42;
    modify_value(x);
    std::cout << x << std::endl;  // Still 42
    return 0;
}

Pass by Pointer

void modify_pointer(int* ptr) {
    if (ptr != nullptr) {
        *ptr = 100;  // Modifies original
    }
}

int main() {
    int x = 42;
    modify_pointer(&x);
    std::cout << x << std::endl;  // 100
    return 0;
}

Pass by Reference

void modify_reference(int& ref) {
    ref = 100;  // Modifies original (simpler than pointer)
}

int main() {
    int x = 42;
    modify_reference(x);
    std::cout << x << std::endl;  // 100
    return 0;
}
void print_large_object(const std::vector<int>& vec) {
    // Can read but not modify
    for (const auto& val : vec) {
        std::cout << val << " ";
    }
}

int main() {
    std::vector<int> large_vec(1000000, 42);
    print_large_object(large_vec);  // Efficient: no copy
    return 0;
}

Scenario 2: Dynamic Memory Allocation

// Allocate single object
int* ptr = new int(42);
std::cout << *ptr << std::endl;  // 42
delete ptr;                       // Must delete
ptr = nullptr;                    // Good practice

// Allocate array
int* arr = new int[10];
for (int i = 0; i < 10; ++i) {
    arr[i] = i;
}
delete[] arr;                     // Use delete[] for arrays
arr = nullptr;

// Common mistake: mismatch
int* ptr2 = new int;
// delete[] ptr2;                 // Wrong: should be delete
delete ptr2;                      // Correct

int* arr2 = new int[10];
// delete arr2;                   // Wrong: should be delete[]
delete[] arr2;                    // Correct

Scenario 3: Array Manipulation

void process_array(int* arr, size_t size) {
    for (size_t i = 0; i < size; ++i) {
        arr[i] *= 2;  // Modify array elements
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    process_array(arr, 5);
    
    for (int val : arr) {
        std::cout << val << " ";  // 2 4 6 8 10
    }
    return 0;
}

// Or using references
void process_array_ref(int (&arr)[5]) {  // Size must match
    for (int& val : arr) {
        val *= 2;
    }
}

Scenario 4: Returning Pointers/References

Return Pointer

int* create_int(int value) {
    int* ptr = new int(value);
    return ptr;  // Caller must delete
}

// Better: return by value or use smart pointer
std::unique_ptr<int> create_int_safe(int value) {
    return std::make_unique<int>(value);
}

Return Reference

int& get_element(int* arr, size_t index) {
    return arr[index];  // Return reference to element
}

int main() {
    int arr[] = {10, 20, 30};
    int& ref = get_element(arr, 1);
    ref = 100;  // Modifies arr[1]
    std::cout << arr[1] << std::endl;  // 100
    return 0;
}

// Dangerous: returning reference to local
int& bad_function() {
    int x = 42;
    return x;  // Dangling reference - undefined behavior!
}

Scenario 5: Linked Data Structures

struct Node {
    int data;
    Node* next;
    
    Node(int d) : data(d), next(nullptr) {}
};

class LinkedList {
public:
    LinkedList() : head(nullptr) {}
    
    void append(int value) {
        Node* new_node = new Node(value);
        if (head == nullptr) {
            head = new_node;
        } else {
            Node* current = head;
            while (current->next != nullptr) {
                current = current->next;
            }
            current->next = new_node;
        }
    }
    
    void print() {
        Node* current = head;
        while (current != nullptr) {
            std::cout << current->data << " ";
            current = current->next;
        }
        std::cout << std::endl;
    }
    
    ~LinkedList() {
        while (head != nullptr) {
            Node* temp = head;
            head = head->next;
            delete temp;
        }
    }
    
private:
    Node* head;
};

// Usage
LinkedList list;
list.append(10);
list.append(20);
list.append(30);
list.print();  // 10 20 30

Scenario 6: Function Pointers

// Function pointer type
using FuncPtr = int(*)(int, int);

int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }

int main() {
    FuncPtr ptr = add;
    std::cout << ptr(3, 4) << std::endl;  // 7
    
    ptr = multiply;
    std::cout << ptr(3, 4) << std::endl;  // 12
    
    // Array of function pointers
    FuncPtr operations[] = {add, multiply};
    std::cout << operations[0](5, 6) << std::endl;  // 11
    std::cout << operations[1](5, 6) << std::endl;  // 30
    
    return 0;
}

Scenario 7: Callback Functions

#include <functional>

class EventHandler {
public:
    using Callback = std::function<void(int)>;
    
    void setCallback(Callback cb) {
        callback_ = cb;
    }
    
    void trigger(int value) {
        if (callback_) {
            callback_(value);
        }
    }
    
private:
    Callback callback_;
};

int main() {
    EventHandler handler;
    
    // Set callback using lambda
    handler.setCallback([](int value) {
        std::cout << "Event: " << value << std::endl;
    });
    
    handler.trigger(42);  // Event: 42
    return 0;
}

Scenario 8: Polymorphism

class Base {
public:
    virtual void print() {
        std::cout << "Base" << std::endl;
    }
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void print() override {
        std::cout << "Derived" << std::endl;
    }
};

int main() {
    // Pointer to base
    Base* ptr = new Derived();
    ptr->print();  // Derived (virtual dispatch)
    delete ptr;
    
    // Reference to base
    Derived d;
    Base& ref = d;
    ref.print();  // Derived (virtual dispatch)
    
    return 0;
}

Scenario 9: Optional Parameters

void process_data(int* data, size_t size, int* result = nullptr) {
    int sum = 0;
    for (size_t i = 0; i < size; ++i) {
        sum += data[i];
    }
    
    if (result != nullptr) {
        *result = sum;
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    
    // Without result
    process_data(arr, 5);
    
    // With result
    int sum;
    process_data(arr, 5, &sum);
    std::cout << "Sum: " << sum << std::endl;  // 15
    
    return 0;
}

Scenario 10: String Manipulation

#include <cstring>

void reverse_string(char* str) {
    if (str == nullptr) return;
    
    size_t len = strlen(str);
    char* start = str;
    char* end = str + len - 1;
    
    while (start < end) {
        char temp = *start;
        *start = *end;
        *end = temp;
        start++;
        end--;
    }
}

int main() {
    char str[] = "Hello";
    reverse_string(str);
    std::cout << str << std::endl;  // "olleH"
    return 0;
}

Scenario 11: Two-Dimensional Arrays

// Static 2D array
int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

// Pointer to first row
int* ptr = matrix[0];
std::cout << ptr[5] << std::endl;  // 6 (row 1, col 1)

// Pointer to pointer (dynamic)
int** dynamic_matrix = new int*[3];
for (int i = 0; i < 3; ++i) {
    dynamic_matrix[i] = new int[4];
}

// Access
dynamic_matrix[1][2] = 42;

// Cleanup
for (int i = 0; i < 3; ++i) {
    delete[] dynamic_matrix[i];
}
delete[] dynamic_matrix;

Scenario 12: Swap Function

// Using pointers
void swap_ptr(int* a, int* b) {
    if (a == nullptr || b == nullptr) return;
    int temp = *a;
    *a = *b;
    *b = temp;
}

// Using references (preferred)
void swap_ref(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10, y = 20;
    
    swap_ptr(&x, &y);
    std::cout << x << " " << y << std::endl;  // 20 10
    
    swap_ref(x, y);
    std::cout << x << " " << y << std::endl;  // 10 20
    
    return 0;
}

Smart Pointers

std::unique_ptr

#include <memory>

// Automatic memory management
{
    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    std::cout << *ptr << std::endl;  // 42
    // Automatically deleted when out of scope
}

// Transfer ownership
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
// std::unique_ptr<int> ptr2 = ptr1;  // Error: cannot copy
std::unique_ptr<int> ptr2 = std::move(ptr1);  // OK: move ownership

std::shared_ptr

#include <memory>

// Shared ownership
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1;  // OK: shared ownership

std::cout << ptr1.use_count() << std::endl;  // 2
std::cout << *ptr1 << std::endl;  // 42
std::cout << *ptr2 << std::endl;  // 42

// Automatically deleted when last reference is destroyed

std::weak_ptr

#include <memory>

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

// Check if object still exists
if (auto locked = weak.lock()) {
    std::cout << *locked << std::endl;  // 42
} else {
    std::cout << "Object destroyed" << std::endl;
}

Smart Pointer vs Raw Pointer

// Raw pointer (manual management)
int* raw_ptr = new int(42);
// ... use raw_ptr ...
delete raw_ptr;  // Must remember to delete

// Smart pointer (automatic management)
std::unique_ptr<int> smart_ptr = std::make_unique<int>(42);
// ... use smart_ptr ...
// Automatically deleted - no manual delete needed

// Prefer smart pointers in modern C++

Best Practices

1. Prefer References Over Pointers When Possible

// Good: Use reference
void process(int& value) {
    value *= 2;
}

// OK but more verbose: Use pointer
void process_ptr(int* value) {
    if (value != nullptr) {
        *value *= 2;
    }
}

2. Use Smart Pointers Instead of Raw Pointers

// Bad: Raw pointer
int* ptr = new int(42);
delete ptr;

// Good: Smart pointer
std::unique_ptr<int> ptr = std::make_unique<int>(42);
// Automatic cleanup

3. Always Initialize Pointers

// Bad: Uninitialized
int* ptr;
*ptr = 42;  // Undefined behavior

// Good: Initialize
int* ptr = nullptr;
if (ptr != nullptr) {
    *ptr = 42;
}

4. Check for Null Before Dereferencing

void safe_dereference(int* ptr) {
    if (ptr != nullptr) {
        *ptr = 42;
    }
}

// Or use short form
if (ptr) {
    *ptr = 42;
}

5. Use Const Correctness

// Const pointer (pointer itself is const)
int x = 10;
int* const ptr = &x;  // Cannot reassign ptr
// ptr = nullptr;     // Error

// Pointer to const (value is const)
const int* ptr2 = &x;  // Cannot modify through ptr2
// *ptr2 = 20;         // Error

// Const pointer to const
const int* const ptr3 = &x;  // Both are const

6. Avoid Dangling Pointers/References

// Bad: Dangling pointer
int* get_ptr() {
    int x = 42;
    return &x;  // x is destroyed when function returns
}

// Bad: Dangling reference
int& get_ref() {
    int x = 42;
    return x;  // x is destroyed when function returns
}

// Good: Return by value or use smart pointer
std::unique_ptr<int> get_safe() {
    return std::make_unique<int>(42);
}

7. Use nullptr Instead of NULL or 0

// Bad: Old style
int* ptr = NULL;
int* ptr2 = 0;

// Good: Modern C++
int* ptr = nullptr;

Common Pitfalls

1. Memory Leaks

// Bad: Memory leak
void leak() {
    int* ptr = new int(42);
    // Forgot to delete - memory leak!
}

// Good: Use smart pointer
void no_leak() {
    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    // Automatically deleted
}

2. Double Delete

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

// Good: Set to nullptr after delete
int* ptr = new int(42);
delete ptr;
ptr = nullptr;
delete ptr;  // Safe: deleting nullptr is no-op

3. Mismatched new/delete

// Bad: Mismatch
int* ptr = new int;
delete[] ptr;  // Wrong!

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

// Good: Match correctly
int* ptr = new int;
delete ptr;    // Correct

int* arr = new int[10];
delete[] arr;  // Correct

4. Returning Address of Local Variable

// Bad: Dangling pointer
int* bad_function() {
    int x = 42;
    return &x;  // x is destroyed!
}

// Good: Return by value or use dynamic allocation
int good_function() {
    int x = 42;
    return x;  // Returns copy
}

5. Pointer Arithmetic on Non-Array

// Bad: Pointer arithmetic on single object
int x = 42;
int* ptr = &x;
ptr++;  // Undefined behavior - points to invalid memory

// Good: Only use pointer arithmetic on arrays
int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;
ptr++;  // OK: points to next element

Summary

Pointers:

  • Store memory addresses
  • Can be null, reassigned, and used for arithmetic
  • Require explicit dereferencing with *
  • Need manual memory management (or use smart pointers)

References:

  • Aliases for existing variables
  • Must be initialized, cannot be reassigned
  • Cannot be null
  • Simpler syntax (no * needed)
  • Automatically dereferenced

Dereferencing:

  • Access value at pointer’s address using *
  • Use -> for member access through pointers
  • Always check for null before dereferencing

Best Practices:

  • Prefer references for function parameters
  • Use smart pointers instead of raw pointers
  • Always initialize pointers
  • Check for null before dereferencing
  • Use nullptr instead of NULL
  • Avoid dangling pointers/references

Understanding these concepts is crucial for effective C++ programming, especially for memory management, polymorphism, and efficient parameter passing.