Skip to content

Pointers — Declaration, Dereferencing, nullptr, void*, and Pointer Arithmetic

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Pointers. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ pointers are variables that hold memory addresses, supporting dereferencing to access the pointed-to value, pointer arithmetic for array traversal, and void* for type-erased memory addressing.

What You'll Learn

You will declare and initialize pointers, dereference them to read and write values, use nullptr for safe null representation, work with void* for type-erased pointers, perform pointer arithmetic on arrays, understand the relationship between pointers and arrays, and avoid dangling pointer and double-free errors.

Why It Matters

Pointers are the most misunderstood concept in C++. They are also the most powerful. Every non-trivial C++ program uses pointers: through iterators, smart pointers, dynamic polymorphism, and resource handles. Understanding pointers means understanding how memory works, which is essential for debugging, performance optimization, and systems programming.

Learning Path

graph LR
    A["20: Copy & Move Semantics"] --> B["21: Pointers"]
    B --> C["22: References"]
    C --> D["23: Dynamic Memory"]
    D --> E["24: Smart Pointers"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style E fill:#4a90d9,stroke:#2c5f8a,color:#fff

Pointer Basics

#include <iostream>

int main() {
    int x = 42;
    
    // Declare a pointer and initialize with the address of x
    int* ptr = &x;
    
    std::cout << "Value of x: " << x << "\n";
    std::cout << "Address of x: " << &x << "\n";
    std::cout << "Pointer value: " << ptr << "\n";
    std::cout << "Dereferenced: " << *ptr << "\n";
    
    // Modify through pointer
    *ptr = 99;
    std::cout << "x after *ptr = 99: " << x << "\n";
}

Expected output (address values vary):

Value of x: 42
Address of x: 0x7fff5fbff7ac
Pointer value: 0x7fff5fbff7ac
Dereferenced: 42
x after *ptr = 99: 99

Pointer Declaration

int* p;           // pointer to int (spacing: int* p is same)
int *p;           // same as above
int* p1, p2;      // p1 is int*, p2 is int (tricky!)
int *p1, *p2;     // both are int*

Rule: Each pointer variable needs its own * prefix. Prefer int* p for clarity, but understand the C-style int *p declaration.

nullptr

#include <iostream>

int main() {
    int* ptr = nullptr;  // modern C++ null pointer
    
    if (ptr == nullptr) {
        std::cout << "Pointer is null\n";
    }
    
    if (ptr) {
        // This block is not executed (nullptr is falsy)
    }
    
    // Dereferencing nullptr is undefined behavior (usually crashes)
    // *ptr = 5;  // DON'T DO THIS
    
    // nullptr is type-safe (unlike NULL macro)
    // NULL is typically 0, which can be confused with integer
    int* p1 = nullptr;    // OK
    // int i = nullptr;   // Error: cannot convert
}

Always initialize pointers to nullptr if you do not have a valid address immediately. Use nullptr rather than NULL or 0.

void* — Type-Erased Pointer

void* can point to any type but cannot be dereferenced directly.

#include <iostream>

int main() {
    int x = 42;
    double y = 3.14;
    
    void* ptr = &x;
    std::cout << "void* address: " << ptr << "\n";
    
    // Must cast back to use
    std::cout << "As int: " << *static_cast<int*>(ptr) << "\n";
    
    ptr = &y;
    std::cout << "As double: " << *static_cast<double*>(ptr) << "\n";
    
    // Cannot dereference void* without cast
    // std::cout << *ptr;  // Error
}

void* is used in low-level memory operations (memcpy, malloc, C-style APIs). In modern C++, templates and variants are preferred.

Pointer Arithmetic

#include <iostream>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int* ptr = arr;  // points to arr[0]
    
    std::cout << *ptr << "\n";         // 10
    std::cout << *(ptr + 1) << "\n";   // 20
    std::cout << *(ptr + 2) << "\n";   // 30
    std::cout << *(ptr + 3) << "\n";   // 40
    std::cout << *(ptr + 4) << "\n";   // 50
    
    // Increment and decrement
    ++ptr;  // now points to arr[1]
    std::cout << *ptr << "\n";  // 20
    
    ptr += 2;  // now points to arr[3]
    std::cout << *ptr << "\n";  // 40
    
    --ptr;  // now points to arr[2]
    std::cout << *ptr << "\n";  // 30
    
    // Pointer difference
    int* start = arr;
    int* end = arr + 5;
    std::cout << "Elements: " << (end - start) << "\n";  // 5
}

Pointer arithmetic automatically adjusts by the size of the pointed-to type. ptr + n advances by n * sizeof(T) bytes.

Pointers and Arrays

#include <iostream>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    
    // arr decays to &arr[0]
    int* p = arr;
    
    // Array indexing is pointer arithmetic
    std::cout << arr[2] << " " << *(arr + 2) << "\n";  // 3 3
    
    // The & operator on an array
    std::cout << arr << "\n";    // address of first element
    std::cout << &arr[0] << "\n"; // same
    std::cout << &arr << "\n";    // same address, but type is int(*)[5]
    
    // arr + 1 vs &arr + 1
    std::cout << "arr + 1: " << (arr + 1) << "\n";    // +4 bytes (next int)
    std::cout << "&arr + 1: " << (&arr + 1) << "\n";   // +20 bytes (next array)
}

Pointers to Pointers

#include <iostream>

int main() {
    int x = 42;
    int* p = &x;
    int** pp = &p;  // pointer to pointer to int
    
    std::cout << x << "\n";         // 42
    std::cout << *p << "\n";        // 42
    std::cout << **pp << "\n";      // 42
    
    **pp = 99;
    std::cout << x << "\n";         // 99
    
    // Used for dynamically allocated 2D arrays
    int** matrix = new int*[3];
    for (int i = 0; i < 3; ++i) {
        matrix[i] = new int[4];
    }
    // ... use matrix ...
    for (int i = 0; i < 3; ++i) delete[] matrix[i];
    delete[] matrix;
}

Function Pointers

#include <iostream>

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

int main() {
    // Declare function pointer
    int (*op)(int, int) = add;
    
    std::cout << op(5, 3) << "\n";  // 8
    
    op = subtract;
    std::cout << op(5, 3) << "\n";  // 2
    
    // Array of function pointers
    int (*operations[])(int, int) = {add, subtract};
    std::cout << operations[0](10, 4) << "\n";  // 14
    std::cout << operations[1](10, 4) << "\n";  // 6
}

Function pointers enable callback mechanisms and plugin architectures. In modern C++, std::function and lambdas are preferred.

Common Mistakes

Mistake 1: Dereferencing Uninitialized Pointer

int* p;
*p = 5;  // undefined behavior: p points to random memory

Always initialize pointers: int* p = nullptr; or int* p = &someVariable;.

Mistake 2: Dangling Pointer

int* p = new int(42);
delete p;
*p = 5;  // dangling pointer: memory has been freed

Set p = nullptr after delete to catch accidental use.

Mistake 3: Memory Leak

int* p = new int(42);
p = new int(99);  // first allocation is leaked
delete p;

Mistake 4: Confusing Pointer and Pointee Types

int x = 42;
double* p = &x;  // Error: type mismatch

Mistake 5: Off-by-One in Pointer Arithmetic

int arr[5];
int* p = arr + 5;  // points one past the end (legal)
*p = 42;           // undefined behavior: dereferencing past the end

Mistake 6: Using delete Instead of delete[]

int* arr = new int[10];
delete arr;  // undefined behavior: should be delete[]

Practice Questions

  1. What is the difference between int* p and int *p?
  2. What does *(arr + 3) mean? How does it relate to arr[3]?
  3. Write a function that swaps two integers using pointers.
  4. What is the output of this code: int x = 5; int* p = &x; int** pp = &p; **pp = 10; cout << x;?
  5. What is the difference between const int* and int* const?

Challenge

Implement a find function that takes a pointer to the beginning of an array, a pointer to one past the end, and a value to search for. Return a pointer to the found element, or nullptr if not found. Test with an integer array.

FAQ

What is the size of a pointer?

On 32-bit systems, 4 bytes. On 64-bit systems, 8 bytes. All pointer types (int*, char*, void*) have the same size on a given platform.

What is a 'dangling pointer'?

A pointer that points to memory that has been freed or that no longer exists. Dereferencing it is undefined behavior.

Is it safe to compare pointers from different arrays?

Comparing pointers from different arrays with <, >, <=, >= is undefined behavior. Equality comparison (==, !=) is always valid.

What is the difference between `int* p` and `int& r`?

A pointer can be reassigned and can be null. A reference cannot be reassigned and must refer to a valid object.

How do I print an address?

Use std::cout << (void*)ptr; to print the numeric address. std::cout << ptr; may print the pointed-to value for char*.

Can I use `delete` on a nullptr?

Yes. Deleting nullptr is safe (does nothing). This is why setting pointers to nullptr after delete is good practice.

Mini Project

Write a simple memory pool allocator using pointers:

#include <iostream>
#include <cstdint>

class PoolAllocator {
private:
    char* memory_;
    size_t size_;
    char* current_;
    
public:
    PoolAllocator(size_t size) : size_(size) {
        memory_ = new char[size];
        current_ = memory_;
    }
    
    ~PoolAllocator() {
        delete[] memory_;
    }
    
    void* allocate(size_t bytes) {
        if (current_ + bytes > memory_ + size_) {
            return nullptr;  // out of memory
        }
        void* block = current_;
        current_ += bytes;
        return block;
    }
    
    void reset() {
        current_ = memory_;
    }
    
    size_t used() const {
        return current_ - memory_;
    }
};

int main() {
    PoolAllocator pool(1024);
    
    int* a = static_cast<int*>(pool.allocate(sizeof(int)));
    *a = 42;
    
    double* b = static_cast<double*>(pool.allocate(sizeof(double)));
    *b = 3.14;
    
    char* c = static_cast<char*>(pool.allocate(10));
    std::snprintf(c, 10, "hello");
    
    std::cout << *a << " " << *b << " " << c << "\n";
    std::cout << "Memory used: " << pool.used() << " / 1024\n";
}

What's Next

Pointers give you direct memory access. The next lesson covers references: lvalue references, rvalue references (C++11), and the critical differences between references and pointers.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro