Skip to content
C++

Raw Pointers: Address-of, Dereference, nullptr

A pointer is a variable that stores a memory address. Every variable in a C++ program lives at some address in memory — a pointer holds that address and lets you access the variable indirectly. Pointers are a foundational concept in C and C++: they underlie references, smart pointers, arrays, and virtually all dynamic memory management. In modern C++, you rarely need raw pointers — smart pointers and references handle most use cases more safely — but understanding raw pointers is essential for reading existing code, working with C APIs, and grasping what the language is doing under the hood.

Declaring a pointer

A pointer type is written as T* where T is the type of the thing being pointed to. An int* points to an int; a double* points to a double. The * is part of the type, not the variable name — place it close to the type for clarity.

int*    p;      // p is a pointer to int — UNINITIALIZED, dangerous!
double* q;      // q is a pointer to double — also uninitialized

// Always initialize pointers:
int*    p = nullptr;    // explicitly "points to nothing"
double* q = nullptr;

An uninitialized pointer contains garbage — a random address. Dereferencing it is undefined behavior and almost always causes a crash. Always initialize pointers to nullptr or to a valid address immediately.

The address-of operator (&) and dereference (*)

The address-of operator & produces the memory address of a variable — you use it to get a pointer to an existing variable. The dereference operator * goes the other direction: given a pointer, it accesses the value stored at that address. Reading through a pointer is called indirection.

int x = 42;

int* p = &x;    // p holds the address of x
                // Example address: 0x7ffe4d3b2a1c

std::cout << x;    // 42     — direct access
std::cout << p;    // 0x7ffe... — the address (a hexadecimal number)
std::cout << *p;   // 42     — indirection: read value at address p

*p = 100;          // write through pointer: x is now 100
std::cout << x;    // 100

The * symbol has two meanings in C++ depending on context: in a declaration (int* p) it declares a pointer type; in an expression (*p) it dereferences a pointer. This overloading confuses beginners — keep the contexts distinct in your mind.

nullptr — the null pointer

nullptris a special value that a pointer can hold to indicate “I point to nothing.” Dereferencing nullptr is undefined behavior and almost always causes an immediate crash (segmentation fault / access violation), which is actually helpful — it fails fast and loudly rather than corrupting data silently. Always test a pointer against nullptr before dereferencing when there is any chance it could be null.

int* p = nullptr;

// Always check before using:
if (p != nullptr) {
    std::cout << *p;     // safe
}

// Shorthand — pointer converts to bool (nullptr = false):
if (p) {
    std::cout << *p;     // same check, shorter
}

// After deleting heap memory, null the pointer:
int* heap = new int(42);
delete heap;
heap = nullptr;          // prevents accidental use-after-free

Pointer arithmetic

You can add or subtract integers from a pointer. The result is a pointer advanced by that many elements (not bytes) — the compiler accounts for the element size automatically. This is how C-style array traversal works: a C array is really just a pointer to its first element.

int arr[] = {10, 20, 30, 40, 50};
int* p = arr;       // p points to arr[0]

std::cout << *p;        // 10
std::cout << *(p + 1);  // 20 — pointer advanced 1 element (4 bytes for int)
std::cout << *(p + 2);  // 30

// Incrementing:
++p;                // p now points to arr[1]
std::cout << *p;    // 20

// Subscript on a pointer is the same as pointer arithmetic:
std::cout << p[2];  // same as *(p + 2) = 30

Pointer arithmetic is only defined within an array (or one-past-the-end). Going outside the array bounds is undefined behavior. This is why modern C++ prefers std::vector and std::span — they carry size information and can bounds-check.

Pointers to structs and classes — the arrow operator

When you have a pointer to a struct or class, you access members with the arrow operator ->. It is shorthand for dereference + dot: p->member means (*p).member.

struct Point { double x; double y; };

Point pt = {3.0, 4.0};
Point* pp = &pt;

std::cout << pp->x;   // 3.0 — same as (*pp).x
pp->x = 10.0;         // modifies pt.x through the pointer

// With heap allocation:
Point* heap_pt = new Point{1.0, 2.0};
std::cout << heap_pt->x;
delete heap_pt;

Common pointer bugs

Dangling pointer
int* p;
{
    int local = 42;
    p = &local;
}   // local is destroyed here
*p = 5;  // UNDEFINED BEHAVIOR — local is gone

A pointer to a variable that no longer exists. The memory may have been reused for something else.

Null dereference
int* p = nullptr;
*p = 5;  // CRASH — segmentation fault

Dereferencing nullptr. Always check before dereferencing.

Double free
int* p = new int(42);
delete p;
delete p;  // UNDEFINED BEHAVIOR

Deleting the same memory twice corrupts the heap allocator.

Memory leak
void leak() {
    int* p = new int(42);
    // ... forgot to delete p
}  // p is gone but memory is not freed

Allocated memory that is never freed. Causes the program to consume more and more memory over time.

Key rules to remember

Always initialize pointers — to nullptr or to a valid address

An uninitialized pointer contains garbage. Dereferencing it is undefined behavior.

Always check for nullptr before dereferencing

if (p) { *p = ...; } Never assume a pointer is non-null unless you have proof.

Never use a pointer after the object it points to has been destroyed

A pointer to a local variable is invalid after the variable goes out of scope. A pointer after delete is a dangling pointer. Set pointers to nullptr after deleting.

In modern C++, prefer smart pointers and references over raw pointers

std::unique_ptr and std::shared_ptr eliminate memory leaks and double-frees. References eliminate null dereferences for required arguments.

Sign in to track progress