Skip to content
C++
Language
since C++11
Beginner

Understand and Use Pointers in C++

Learn to declare, dereference, and pass pointers so you can work directly with memory and write functions that modify their arguments.

By the end of this page, you will be able to declare a pointer, read and write through it, pass it to a function so the function can modify the original variable, and recognize the three most common pointer mistakes before they crash your program.

What and Why

Every variable in your program lives somewhere in memory. Think of memory as a long street of numbered houses β€” each house holds one value, and its address is a number like 0x7ffee4b2c8a0. Normally you refer to a variable by its name (int x = 5;) and the compiler handles the address for you.

A pointer is a variable that holds one of those addresses. Instead of storing a number like 5, it stores the location where a 5 lives. This matters for three big reasons:

  1. Shared mutation β€” a function can receive the address of your variable and change the original, not a copy.
  2. Efficient data passing β€” copying a 4-byte address is cheaper than copying a 400-byte struct.
  3. Dynamic memory β€” allocating objects whose size or lifetime is not known until runtime requires pointers.

Modern C++ gives you safer abstractions (references, smart pointers), but you need to understand raw pointers first because the rest of the language is built on them.

Step by Step

1 β€” Get an address with &

The address-of operator & gives you the memory address of any variable.

cpp
#include <iostream>

int main() {
    int score = 42;
    std::cout << "value:   " << score  << "\n";
    std::cout << "address: " << &score << "\n";
}

Run this and you will see something like:

cpp
value:   42
address: 0x7ffc1234abcd

The exact address changes every run, but the point stands: every variable has one.

2 β€” Declare a pointer

A pointer type is written as the pointee type followed by *. Read int* as "pointer to int".

cpp
#include <iostream>

int main() {
    int score = 42;
    int* ptr = &score;   // ptr holds the address of score

    std::cout << "ptr holds:  " << ptr  << "\n";   // an address
    std::cout << "score is at:" << &score << "\n"; // same address
}

ptr and &score print the same number, confirming that ptr genuinely points at score.

3 β€” Read through a pointer with *

The dereference operator * follows the address and gives you the value stored there.

cpp
#include <iostream>

int main() {
    int score = 42;
    int* ptr = &score;

    std::cout << *ptr << "\n";  // prints 42
}

Reading the pointer itself (ptr) gives an address. Dereferencing it (*ptr) gives the value at that address.

4 β€” Write through a pointer

You can also assign through a dereference, which changes the original variable.

cpp
#include <iostream>

int main() {
    int score = 42;
    int* ptr = &score;

    *ptr = 100;  // writes 100 into the memory that ptr points at

    std::cout << score << "\n";  // prints 100 β€” score itself changed
}

score and *ptr are two names for the same memory location. Writing to either one is the same operation.

5 β€” Pass a pointer to a function

This is the most important real-world use. Without a pointer (or reference), a function receives a copy and cannot change the original:

cpp
#include <iostream>

void addTen(int value) {
    value += 10;  // modifies only the local copy
}

int main() {
    int score = 42;
    addTen(score);
    std::cout << score << "\n";  // still 42
}

With a pointer, the function receives the address and can modify the original:

cpp
#include <iostream>

void addTen(int* value) {
    *value += 10;  // modifies the original through the pointer
}

int main() {
    int score = 42;
    addTen(&score);              // pass the address
    std::cout << score << "\n"; // now 52
}

Common Patterns

Pattern 1 β€” Output parameters

When a function needs to return more than one result, pointers (or references) let it write into caller-provided variables.

cpp
#include <iostream>

void minmax(const int* arr, int len, int* out_min, int* out_max) {
    *out_min = arr[0];
    *out_max = arr[0];
    for (int i = 1; i < len; ++i) {
        if (arr[i] < *out_min) *out_min = arr[i];
        if (arr[i] > *out_max) *out_max = arr[i];
    }
}

int main() {
    int data[] = {3, 1, 4, 1, 5, 9, 2, 6};
    int lo, hi;
    minmax(data, 8, &lo, &hi);
    std::cout << "min=" << lo << " max=" << hi << "\n";
}

Pattern 2 β€” Nullable parameter (optional input)

A pointer can be nullptr β€” the null address β€” to signal "no value provided". A reference cannot do this.

cpp
#include <iostream>

void greet(const char* name) {
    if (name == nullptr) {
        std::cout << "Hello, stranger!\n";
    } else {
        std::cout << "Hello, " << name << "!\n";
    }
}

int main() {
    greet("Alice");   // Hello, Alice!
    greet(nullptr);   // Hello, stranger!
}

Pattern 3 β€” Pointer arithmetic with arrays

An array name decays to a pointer to its first element. You can walk the array by incrementing the pointer.

cpp
#include <iostream>

int main() {
    int nums[] = {10, 20, 30, 40, 50};
    int* p = nums;  // points at nums[0]

    for (int i = 0; i < 5; ++i) {
        std::cout << *(p + i) << " ";  // p+i is the address of nums[i]
    }
    std::cout << "\n";
}

p + i advances the address by i * sizeof(int) bytes β€” the compiler scales the offset automatically.

What Can Go Wrong

Mistake 1 β€” Dereferencing an uninitialized pointer

cpp
// WRONG β€” undefined behavior, likely crash
int* p;
*p = 5;

p contains garbage. Dereferencing it reads or writes a random memory location. Always initialize pointers before use:

cpp
int value = 0;
int* p = &value;  // safe: p holds a valid address
*p = 5;

Mistake 2 β€” Dereferencing nullptr

cpp
// WRONG β€” immediate crash (segfault)
int* p = nullptr;
std::cout << *p;

Whenever a pointer might be null β€” especially one received as a parameter β€” check before dereferencing:

cpp
void print(int* p) {
    if (p != nullptr) {
        std::cout << *p << "\n";
    }
}

Mistake 3 β€” Dangling pointer (pointing to a destroyed variable)

cpp
// WRONG β€” ptr survives past score's lifetime
int* ptr;
{
    int score = 42;
    ptr = &score;
}  // score is destroyed here
*ptr = 10;  // undefined behavior: the memory no longer belongs to score

Never let a pointer outlive the object it points to. If you need an object to outlive its enclosing scope, allocate it on the heap and manage it with a smart pointer (see smart pointers).

Mistake 4 β€” Confusing * in declaration vs. expression

* means two different things depending on where it appears:

cpp
int* ptr = &x;  // declaration: ptr is a pointer-to-int
*ptr = 5;       // expression: dereference ptr, write 5

The declaration * is part of the type. The expression * is an operator. They look the same but do completely different things.

Quick Reference

SyntaxMeaning
int* p;Declare p as a pointer to int
p = &x;Store the address of x in p
*pRead the value at the address held by p
*p = v;Write v to the address held by p
p == nullptrCheck whether p holds the null address
p + nAddress of the element n positions ahead (pointer arithmetic)
p++Advance p to the next element (increments by sizeof(*p) bytes)

Rules to keep in mind:

  • Always initialize a pointer before using it.
  • Always check for nullptr before dereferencing a pointer that might be null.
  • Never dereference a pointer to a variable that has gone out of scope.
  • Prefer references when the pointer will never be null and never needs to be reseated.

What's Next

  • Pointers β€” language reference β€” the full specification of pointer types, cv-qualifiers, and pointer conversions.
  • Function Pointers β€” store and call functions through a pointer, the building block of callbacks.
  • Smart Pointers β€” unique_ptr and shared_ptr automate lifetime management so you rarely need raw new/delete.
  • References β€” safer, non-nullable alternative to pointers for most everyday use cases.