Skip to content

Pointers and References

Understanding how computer memory is structured and how variables access it is what gives C++ its low-level efficiency and control. While higher-level languages hide memory addresses, C++ gives you direct control over them through pointers and references.


1. Computer Memory and Pointers

Every variable you declare is stored in the computer's Random Access Memory (RAM). Each byte of RAM has a unique number assigned to it, which is its memory address.

    Variable 'num' (value = 42)
    ┌───────────┐
    │    42     │  ◄── Memory Value
    └───────────┘
     0x7ffd51a0    ◄── Memory Address (Hexadecimal)
      int* ptr     ◄── Pointer variable containing 0x7ffd51a0

A pointer is a variable that stores a memory address as its value, rather than a standard data type.

Pointer Declaration and Direct Access Operators

To work with pointers, C++ provides two primary operators: - & (Address-of Operator): Retrieves the memory address of a variable. - * (Dereference/Indirection Operator): Accesses the data stored at the memory address contained within a pointer.

#include <iostream>

int main() {
    int num = 42;

    // Declare a pointer to an integer, initialized to the address of 'num'
    int* ptr = &num; 

    std::cout << "Value of num: " << num << "\n";          // Prints: 42
    std::cout << "Address of num (&num): " << &num << "\n";  // Prints: e.g., 0x7ffd51a0
    std::cout << "Pointer value (ptr): " << ptr << "\n";    // Prints: e.g., 0x7ffd51a0

    // Dereference pointer to read value
    std::cout << "Value pointed to (*ptr): " << *ptr << "\n"; // Prints: 42

    // Dereference pointer to write value
    *ptr = 99;
    std::cout << "New value of num: " << num << "\n";      // Prints: 99

    return 0;
}

Pointer Sizes

Because pointers store memory addresses, their size depends strictly on the CPU architecture of your system, not the data type they point to: - On 32-bit operating systems (e.g., legacy Raspberry Pi OS), all pointers are 4 bytes. - On 64-bit operating systems (e.g., modern Raspberry Pi OS running ARM64), all pointers are 8 bytes.


2. Safe Pointer Initialization: nullptr

Uninitialized pointers contain garbage addresses. Attempting to dereference an uninitialized pointer leads to Undefined Behavior (UB), typically causing a segmentation fault (crash) or memory corruption.

Always initialize your pointers. If you do not have a valid address immediately, initialize the pointer to nullptr.

1
2
3
4
5
6
int* ptr1;           // Dangerous! Contains a random memory location.
int* ptr2 = nullptr; // Safe. Points to a defined null address.

if (ptr2 != nullptr) {
    std::cout << *ptr2; // Only dereference if pointer is valid
}

Why nullptr is Better than NULL or 0

In old C and C++98, NULL or 0 was used to represent a null pointer. However, NULL is simply a macro for the integer 0. This caused type-safety problems and function overloading issues:

1
2
3
4
5
void log(int x);     // Overload A
void log(int* ptr);   // Overload B

log(NULL); // Ambiguous! NULL is an integer, so this might call Overload A instead of B.
log(nullptr); // Safe! 'nullptr' has type 'std::nullptr_t', correctly calling Overload B.

3. Pointers and Constants: The const Matrix

Combining pointers with the const keyword restricts modification. The placement of the const keyword determines what is immutable:

1. Pointer to Constant Data (const T*)

The data pointed to cannot be modified through this pointer, but the pointer itself can point to a different address.

1
2
3
4
5
int x = 10, y = 20;
const int* ptr = &x;

// *ptr = 15; // ERROR: data is read-only
ptr = &y;     // OK: pointer can change addresses

2. Constant Pointer (T* const)

The pointer itself is constant and cannot point to another address, but the data it points to can be modified.

1
2
3
4
5
int x = 10, y = 20;
int* const ptr = &x;

*ptr = 15;   // OK: data can change
// ptr = &y; // ERROR: pointer address is read-only

3. Constant Pointer to Constant Data (const T* const)

Neither the pointer nor the data can be modified.

const int x = 10;
const int* const ptr = &x;

Memory Aid: Read pointer declarations from right to left: - int* const ptr $\rightarrow$ ptr is a const pointer to an int. - const int* ptr $\rightarrow$ ptr is a pointer to an int which is const.


4. References

A reference is an alias (another name) for an existing variable. It behaves like a constant pointer that is automatically dereferenced by the compiler.

1
2
3
4
5
int original = 100;
int& ref = original; // 'ref' is now a synonym for 'original'

ref = 200; // Modifies 'original' to 200
std::cout << original << " " << ref << "\n"; // Prints: 200 200

Essential Rules of References

  1. Must be Initialized: You cannot declare a reference without binding it to an object.
    // int& ref; // Compile Error!
    
  2. Cannot be Rebound: Once a reference is created, it cannot be changed to refer to another variable. Any assignment to the reference changes the value of the underlying variable.
  3. Cannot be Null: A reference must always refer to a valid object in memory.

5. Pointers vs. References: Feature Matrix

Feature Pointer (T*) Reference (T&)
Nullability Can be nullptr Must refer to a valid object
Reassignable Yes (can point to a different variable) No (bound to the initial variable for life)
Initialization Optional (but highly recommended) Mandatory at declaration
Syntax Explicit dereference using * and -> Direct access (behaves like a standard variable)
Arithmetic Supported (ptr++, ptr + 5) Not supported
Size 4 or 8 bytes (stores address) Typically compiler-optimized out (shares object size)

6. Pointer Arithmetic and Arrays

Arrays in C++ are stored as contiguous blocks of memory. When you pass an array to a function, it decays into a pointer pointing to its first element.

#include <iostream>

int main() {
    int arr[] = {10, 20, 30, 40};
    int* ptr = arr; // Points to arr[0] (value 10)

    std::cout << "arr[0] via pointer: " << *ptr << "\n"; // Prints 10

    // Pointer Arithmetic: increments address by sizeof(int) (4 bytes)
    ptr++; 
    std::cout << "arr[1] via pointer: " << *ptr << "\n"; // Prints 20

    // Access with offset index
    std::cout << "arr[3] via offset: " << *(ptr + 2) << "\n"; // Prints 40

    return 0;
}